Repository: IntelPython/numba-dpex (language: python)

Issue:
Remove spirv-tool call generators from spirv_generator.py.
- [X] Have you provided a meaningful PR description?

    - The spirv-tool package provides various utilities to
      disassemble, validate and potentially optimize a
      SPIR-V binary file. Numba-dpex no longer has spirv-tools
      as a dependency and it is up to the user if they want to
      try out the funtionalities provided by spirv-tools.

      Having the option to call spirv-tool utilities from
      spirv_generator is unused and potentially confusing. The
      commit removes all such options and renames the helper
      class from CmdLine to _SprivGenerator.

    - Also removed the config option to run spirv validator.

- [ ] Have you added a test, reproducer or referred to an issue with a reproducer?
- [x] Have you tested your changes locally for CPU and GPU devices?
- [x] Have you made sure that new changes do not introduce compiler warnings?
- [ ] If this PR is a work in progress, are you filing the PR as a draft?



Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
index 933ea1797f..1f1b7feeb4 100644
--- a/.flake8
+++ b/.flake8
@@ -13,9 +13,12 @@ per-file-ignores =
     __init__.py: F403
     # module level import not at top of file
     target.py: E402
+    # config should containe code lines examples in comment
+    docs/source/conf.py: E800
 exclude =
     .git,
     __pycache__,
     _version.py,
+    versioneer.py,
     lowerer.py,
     parfor.py
diff --git a/.github/workflows/conda-package.yml b/.github/workflows/conda-package.yml
index 38d4b4797f..2f8eada12b 100644
--- a/.github/workflows/conda-package.yml
+++ b/.github/workflows/conda-package.yml
@@ -14,10 +14,11 @@ env:
   PACKAGE_NAME: numba-dpex
   MODULE_NAME: numba_dpex
   # There is a separate action that removes defaults.
-  CHANNELS: 'dppy/label/dev,conda-forge,intel'
+  CHANNELS: 'dppy/label/dev,conda-forge,intel,numba,nodefaults'
   VER_JSON_NAME: 'version.json'
   VER_SCRIPT1: "import json; f = open('version.json', 'r'); j = json.load(f); f.close(); "
   VER_SCRIPT2: "d = j['numba-dpex'][0]; print('='.join((d[s] for s in ('version', 'build'))))"
+  PYTHONIOENCODING: 'utf-8'
 
 jobs:
   build:
@@ -44,7 +45,7 @@ jobs:
           access_token: ${{ github.token }}
 
       - name: Checkout ${{ env.PACKAGE_NAME }} repo
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
@@ -63,7 +64,10 @@ jobs:
         run: conda config --remove channels defaults
 
       - name: Store conda paths as envs
-        run: echo "CONDA_BLD=$CONDA_PREFIX/conda-bld/${{ runner.os == 'Linux' && 'linux' || 'win' }}-64/" | tr "\\" '/' >> $GITHUB_ENV
+        run: |
+          echo "CONDA_BLD=$CONDA_PREFIX/conda-bld/${{ runner.os == 'Linux' && 'linux' || 'win' }}-64/" | tr "\\" '/' >> $GITHUB_ENV
+          echo "WHEELS_OUTPUT_FOLDER=$GITHUB_WORKSPACE${{ runner.os == 'Linux' && '/' || '\\' }}" >> $GITHUB_ENV
+          echo "WHEELS_NAME=$PACKAGE_NAME" | tr "-" "_" >> $GITHUB_ENV
 
       # boa is an extention to conda so we can use mamba resolver in conda build
       - name: Install conda-build
@@ -78,6 +82,11 @@ jobs:
           name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }}
           path: ${{ env.CONDA_BLD }}${{ env.PACKAGE_NAME }}-*.tar.bz2
 
+      - name: Upload wheels artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Wheels Python ${{ matrix.python }}
+          path: ${{ env.WHEELS_OUTPUT_FOLDER }}${{ env.WHEELS_NAME }}-*.whl
 
   test:
     name: Test ['${{ matrix.os }}', python='${{ matrix.python }}']
@@ -96,8 +105,13 @@ jobs:
         python: ['3.9', '3.10', '3.11']
         os: [ubuntu-20.04, ubuntu-latest, windows-latest]
         experimental: [false]
+        use_mlir: [false]
+        run_gdb: [true, false]
+        exclude:
+          - os: windows-latest
+            run_gdb: true
 
-    continue-on-error: ${{ matrix.experimental }}
+    continue-on-error: ${{ matrix.experimental || matrix.use_mlir }}
 
     steps:
       - name: Setup miniconda
@@ -159,7 +173,11 @@ jobs:
       # We want to make sure that all dependecies install automatically.
       # intel::intel-opencl-rt is needed for set-intel-ocl-icd-registry.ps1
       - name: Install builded package
-        run: mamba install ${{ env.PACKAGE_NAME }}=${{ env.PACKAGE_VERSION }} intel::intel-opencl-rt pytest -c ${{ env.CHANNEL_PATH }}
+        run: mamba install ${{ env.PACKAGE_NAME }}=${{ env.PACKAGE_VERSION }} intel::intel-opencl-rt pytest-cov conda-tree -c ${{ env.CHANNEL_PATH }}
+
+      - name: Install numba-mlir
+        if: matrix.use_mlir
+        run: mamba install numba-mlir -c dppy/label/dev -c conda-forge -c intel
 
       - name: Setup OpenCL CPU device
         if: runner.os == 'Windows'
@@ -168,6 +186,13 @@ jobs:
           $script_path="$env:CONDA_PREFIX\Scripts\set-intel-ocl-icd-registry.ps1"
           &$script_path
 
+      - name: Checkout setup config
+        uses: actions/checkout@v4
+        with:
+          sparse-checkout: |
+            pyproject.toml
+          sparse-checkout-cone-mode: false
+
       - name: List installed packages
         run: conda list
 
@@ -175,14 +200,28 @@ jobs:
         run: |
           python -c "import dpcpp_llvm_spirv as p; print(p.get_llvm_spirv_path())"
 
+      - name: Check dependency tree
+        run: conda-tree depends -t numba-dpex
+
       - name: Smoke test
+        env:
+          NUMBA_DPEX_USE_MLIR: ${{ matrix.use_mlir && '1' || '0'  }}
         run: python -c "import dpnp, dpctl, numba_dpex; dpctl.lsplatform()"
 
+      - name: Smoke test - test import with no default device
+        env:
+          ONEAPI_DEVICE_SELECTOR: unknown:unknown
+        run: python -c "import numba_dpex"
+
       - name: Run tests
+        if: ${{!matrix.run_gdb}}
+        env:
+          NUMBA_DPEX_USE_MLIR: ${{ matrix.use_mlir && '1' || '0'  }}
         run: |
           pytest -q -ra --disable-warnings --pyargs ${{ env.MODULE_NAME }} -vv
 
       - name: Run examples
+        if: ${{!matrix.run_gdb}}
         shell: bash -l {0}
         run: |
           cd ${{ env.EXAMPLES_PATH }}
@@ -192,6 +231,21 @@ jobs:
             python ${script} || exit 1
           done
 
+      - name: Run gdb tests
+        if: ${{matrix.run_gdb}}
+        env:
+          GDB_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/fcea1bcd-6a63-4849-b304-906ff71dc2c0/l_dpcpp_dbg_p_2023.2.0.49333_offline.sh
+          GDB_INSTALLER: l_dpcpp_dbg_p_2023.2.0.49333_offline.sh
+        run: |
+          conda install pexpect
+          wget -nc -q ${{ env.GDB_URL }}
+          chmod +x ${{ env.GDB_INSTALLER }}
+          mkdir /tmp/gdb
+          export ONEAPI_ROOT=/tmp/gdb
+          ./${{ env.GDB_INSTALLER }} -a -s --eula accept --install-dir $ONEAPI_ROOT
+          source $ONEAPI_ROOT/debugger/latest/env/vars.sh
+          pytest -q -ra --disable-warnings --pyargs ${{ env.MODULE_NAME }}.tests.debugging -vv
+
   upload_anaconda:
     name: Upload dppy/label/dev ['${{ matrix.os }}', python='${{ matrix.python }}']
     if: ${{github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/heads/release') == true)}}
@@ -217,6 +271,11 @@ jobs:
         with:
           name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }}
 
+      - name: Download wheels artifact
+        uses: actions/download-artifact@v3
+        with:
+          name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Wheels Python ${{ matrix.python }}
+
       - name: Setup miniconda
         uses: conda-incubator/setup-miniconda@v2
         with:
@@ -233,3 +292,43 @@ jobs:
 
       - name: Upload
         run: anaconda --token ${{ secrets.ANACONDA_TOKEN }} upload --user dppy --label dev ${{ env.PACKAGE_NAME }}-*.tar.bz2
+
+      - name: Package version
+        run: echo "PACKAGE_VERSION=$(basename ${{ env.PACKAGE_NAME }}-*.tar.bz2 | sed 's/^${{ env.PACKAGE_NAME }}-\([^-]*\).*/\1/')" >> $GITHUB_ENV
+
+      - name: Store wheels name
+        run: |
+          echo "WHEELS_NAME=$PACKAGE_NAME" | tr "-" "_" >> $GITHUB_ENV
+
+      - name: Upload Wheels
+        run: anaconda --token ${{ secrets.ANACONDA_TOKEN }} upload --user dppy --label dev ${{ env.WHEELS_NAME }}-*.whl --version ${{ env.PACKAGE_VERSION }}
+
+  cleanup_packages:
+    name: Clean up anaconda packages
+    needs: [upload_anaconda]
+    runs-on: 'ubuntu-latest'
+    defaults:
+      run:
+        shell: bash -el {0}
+    steps:
+      - uses: conda-incubator/setup-miniconda@v2
+        with:
+          run-post: false
+          channel-priority: "disabled"
+          channels: conda-forge
+          python-version: '3.11'
+
+      - name: Install anaconda-client
+        run: conda install anaconda-client
+
+      - name: Checkout repo
+        uses: actions/checkout@v4
+        with:
+          repository: IntelPython/devops-tools
+          fetch-depth: 0
+
+      - name: Cleanup old packages
+        run: |
+          python scripts/cleanup-old-packages.py \
+          --verbose --force --token ${{ secrets.ANACONDA_TOKEN }} \
+          --package dppy/${{ env.PACKAGE_NAME }} --label dev
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index d615760451..9f3349a70b 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -7,6 +7,7 @@ on:
     paths:
       - 'numba_dpex/**'
       - environment/coverage.yml
+      - pyproject.toml
 
 jobs:
   main:
@@ -38,7 +39,7 @@ jobs:
       - name: Build numba-dpex
         run: |
           export PATH=$CONDA/bin-llvm:$PATH
-          python setup.py develop
+          CC=icx CXX=icpx python setup.py develop
 
       - name: Test installation
         run: |
@@ -52,7 +53,7 @@ jobs:
 
       - name: Run tests with coverage
         run: |
-          pytest -q -ra --disable-warnings --cov --cov-report term-missing --pyargs numba_dpex -vv
+          pytest  -q --cov --cov-report term-missing --pyargs numba_dpex
 
       - name: Install coveralls
         shell: bash -l {0}
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 8270f45f26..e1123c33c0 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -12,7 +12,7 @@ on:
       numpy_version:
         default: '1.24.3'
       numba_version:
-        default: '0.57.0'
+        default: '0.58'
       # dpcpp_llvm_spirv_version:
       #   default: 'main'
       cython_version:
diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
index 920881a652..c4e2479a55 100644
--- a/.github/workflows/gh-pages.yml
+++ b/.github/workflows/gh-pages.yml
@@ -11,6 +11,7 @@ on:
     paths:
       - 'docs/**'
       - environment/docs.yml
+      - .github/workflows/gh-pages.yml
 
 jobs:
   main:
@@ -38,7 +39,7 @@ jobs:
       - name: Build numba-dpex
         run: |
           export PATH=$CONDA/bin-llvm:$PATH
-          python setup.py develop
+          CC=icx CXX=icpx python setup.py develop
 
       - name: Make documentation
         working-directory: docs
diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml
index c66d4234d8..bb7880bebf 100644
--- a/.github/workflows/license.yml
+++ b/.github/workflows/license.yml
@@ -10,14 +10,14 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v2
-    - uses: actions/setup-go@v2
+    - uses: actions/checkout@v3
+    - uses: actions/setup-go@v3
       with:
-        go-version: '1.16'
+        go-version: '1.18'
     - name: Install addlicense
       run: |
         export PATH=${PATH}:`go env GOPATH`/bin
-        go get -v -u github.com/google/addlicense
+        go install github.com/google/addlicense@latest
     - name: Check license
       run: |
         export PATH=${PATH}:`go env GOPATH`/bin
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index dd2df25774..540a7d3c73 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -8,7 +8,19 @@ on:
 jobs:
   pre-commit:
     runs-on: ubuntu-20.04
+    defaults:
+      run:
+        shell: bash -el {0}
     steps:
-    - uses: actions/checkout@v2
-    - uses: actions/setup-python@v2
-    - uses: pre-commit/action@v2.0.0
+    - uses: actions/checkout@v3
+    - uses: conda-incubator/setup-miniconda@v2
+      with:
+        python-version: '3.11'
+        activate-environment: "coverage"
+        channel-priority: "disabled"
+        environment-file: environment/pre-commit.yml
+    - uses: actions/cache@v3
+      with:
+        path: ~/.cache/pre-commit
+        key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
+    - run: pre-commit run --show-diff-on-failure --color=always --all-files
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index aa7371eaec..2411834df1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -35,6 +35,7 @@ repos:
     rev: 3.9.2
     hooks:
     -   id: flake8
+        additional_dependencies: [flake8-eradicate]
 -   repo: https://github.com/koalaman/shellcheck-precommit
     rev: v0.8.0
     hooks:
@@ -46,3 +47,17 @@ repos:
         args: ["-i"]
         exclude: "numba_dpex/dpnp_iface"
         types_or: [c++, c]
+-   repo: local
+    hooks:
+    - id: pylint
+      name: pylint
+      entry: pylint
+      files: ^numba_dpex/experimental|^numba_dpex/core/utils/kernel_launcher.py
+      language: system
+      types: [python]
+      require_serial: true
+      args:
+        [
+          "-rn", # Only display messages
+          "-sn", # Don't display the score
+        ]
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 44fd0cd8bb..e1dc88df2c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -25,10 +25,4 @@ project(numba-dpex
     VERSION ${NUMBA_DPEX_VERSION}
 )
 
-if(IS_INSTALL)
-    install(DIRECTORY numba_dpex
-        DESTINATION ${CMAKE_INSTALL_PREFIX}
-        FILES_MATCHING PATTERN "*.py")
-endif()
-
 add_subdirectory(numba_dpex)
diff --git a/Dockerfile b/Dockerfile
index 1bfa3f78ed..538eb44c8b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -42,7 +42,7 @@ ARG CMAKE_VERSION_BUILD=4
 
 # Python
 ARG INTEL_NUMPY_VERSION="==1.24.3"
-ARG INTEL_NUMBA_VERSION="==0.57.0"
+ARG INTEL_NUMBA_VERSION="==0.58"
 ARG CYTHON_VERSION="==0.29.35"
 ARG SCIKIT_BUILD_VERSION="==0.17.6"
 
diff --git a/LICENSES.third-party b/LICENSES.third-party
new file mode 100644
index 0000000000..14d3cf2938
--- /dev/null
+++ b/LICENSES.third-party
@@ -0,0 +1,57 @@
+The numba-dpex source tree includes vendored libraries governed by the following
+licenses.
+
+
+boost hash.hpp header
+---------------------
+
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+numba
+------------------------
+Copyright (c) 2012, Anaconda, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 9f90b3d24e..6fa00b9f3c 100644
--- a/README.md
+++ b/README.md
@@ -23,19 +23,20 @@ code-generation for the kernel API currently supports
 [OpenCL](https://www.khronos.org/opencl/) and
 [oneAPI Level Zero](https://spec.oneapi.io/level-zero/latest/index.html)
 devices that are supported by Intel&reg; DPC++ SYCL compiler runtime. Supported
-devices include Intel&reg CPUs, integrated GPUs and discrete GPUs.
+devices include Intel&reg; CPUs, integrated GPUs and discrete GPUs.
 
 The offload functionality in numba-dpex is based on Numba's `parfor`
 loop-parallelizer. Our compiler extends Numba's `parfor` feature to generate
 kernels and offload them to devices supported by DPC++ SYCL compiler runtime.
 The offload functionality is supported via a new NumPy drop-in replacement
-library: [dpnp](https://github.com/IntelPython/dpnp) and NumPy-based expressions
-and `numba.prange` loops are not offloaded.
+library: [dpnp](https://github.com/IntelPython/dpnp). Note that `dpnp` and NumPy-based
+expressions can be used together in the same function, with `dpnp` expressions getting
+offloaded by `numba-dpex` and NumPy expressions getting parallelized by Numba.
 
 Refer the [documentation](https://intelpython.github.io/numba-dpex) and examples
 to learn more.
 
-## Getting Started
+# Getting Started
 
 Numba-dpex is part of the Intel&reg; Distribution of Python (IDP) and Intel&reg;
 oneAPI AIKit, and can be installed along with oneAPI. Additionally, we support
@@ -51,7 +52,25 @@ follows:
 python -m pytest --pyargs numba_dpex.tests
 ```
 
-## Contributing
+## Conda
+
+To install `numba_dpex` from the Intel(R) channel on Anaconda
+cloud, use the following command:
+
+```bash
+conda install numba-dpex -c intel -c conda-forge
+```
+
+## Pip
+
+The `numba_dpex` can be installed using `pip` obtaining wheel packages either from PyPi or from Intel(R) channel on Anaconda.
+To install `numba_dpex` wheel package from Intel(R) channel on Anaconda, run the following command:
+
+```bash
+python -m pip install --index-url https://pypi.anaconda.org/intel/simple numba-dpex
+```
+
+# Contributing
 
 Please create an issue for feature requests and bug reports. You can also use
 the GitHub Discussions feature for general questions.
diff --git a/conda-recipe/bld.bat b/conda-recipe/bld.bat
index 8926930b24..33741e024b 100644
--- a/conda-recipe/bld.bat
+++ b/conda-recipe/bld.bat
@@ -3,11 +3,28 @@
 @REM used BUILD_PREFIX as compiler installed in build section of meta.yml
 set "PATH=%BUILD_PREFIX%\Library\bin-llvm;%PATH%"
 
-%PYTHON% setup.py install --single-version-externally-managed --record=record.txt
+REM A workaround for activate-dpcpp.bat issue to be addressed in 2021.4
+set "LIB=%BUILD_PREFIX%\Library\lib;%BUILD_PREFIX%\compiler\lib;%LIB%"
+SET "INCLUDE=%BUILD_PREFIX%\include;%INCLUDE%"
+
+REM Since the 60.0.0 release, setuptools includes a local, vendored copy
+REM of distutils (from late copies of CPython) that is enabled by default.
+REM It breaks build for Windows, so use distutils from "stdlib" as before.
+REM @TODO: remove the setting, once transition to build backend on Windows
+REM to cmake is complete.
+SET "SETUPTOOLS_USE_DISTUTILS=stdlib"
+
+set "CC=icx"
+set "CXX=icx"
+
+set "SKBUILD_ARGS=-G Ninja --"
+set "SKBUILD_ARGS=%SKBUILD_ARGS% -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON"
+
+%PYTHON% setup.py install --single-version-externally-managed --record=record.txt %SKBUILD_ARGS%
 
 rem Build wheel package
 if NOT "%WHEELS_OUTPUT_FOLDER%"=="" (
-    %PYTHON% setup.py bdist_wheel
+    %PYTHON% setup.py bdist_wheel --build-number %GIT_DESCRIBE_NUMBER% %SKBUILD_ARGS%
     if errorlevel 1 exit 1
     copy dist\numba_dpex*.whl %WHEELS_OUTPUT_FOLDER%
     if errorlevel 1 exit 1
diff --git a/conda-recipe/build.sh b/conda-recipe/build.sh
index 98721196f9..84833176a4 100644
--- a/conda-recipe/build.sh
+++ b/conda-recipe/build.sh
@@ -2,19 +2,30 @@
 
 set -euxo pipefail
 
+# Intel LLVM must cooperate with compiler and sysroot from conda
+export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:${BUILD_PREFIX}/lib"
+
+echo "--gcc-toolchain=${BUILD_PREFIX} --sysroot=${BUILD_PREFIX}/${HOST}/sysroot -target ${HOST}" > icpx_for_conda.cfg
+ICPXCFG="$(pwd)/icpx_for_conda.cfg"
+ICXCFG="$(pwd)/icpx_for_conda.cfg"
+
+export ICXCFG
+export ICPXCFG
+
+export CC=icx
+export CXX=icpx
+
 # new llvm-spirv location
 # starting from dpcpp_impl_linux-64=2022.0.0=intel_3610
 export PATH=$CONDA_PREFIX/bin-llvm:$PATH
 
-${PYTHON} setup.py install --single-version-externally-managed --record=record.txt
+SKBUILD_ARGS=(-G Ninja -- -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON)
+
+${PYTHON} setup.py install --single-version-externally-managed --record=record.txt "${SKBUILD_ARGS[@]}"
 
 # Build wheel package
-if [ "$CONDA_PY" == "36" ]; then
-    WHEELS_BUILD_ARGS=(-p manylinux1_x86_64)
-else
-    WHEELS_BUILD_ARGS=(-p manylinux2014_x86_64)
-fi
+WHEELS_BUILD_ARGS=(-p manylinux2014_x86_64 --build-number "$GIT_DESCRIBE_NUMBER")
 if [[ -v WHEELS_OUTPUT_FOLDER ]]; then
-    $PYTHON setup.py bdist_wheel "${WHEELS_BUILD_ARGS[@]}"
+    $PYTHON setup.py bdist_wheel "${WHEELS_BUILD_ARGS[@]}" "${SKBUILD_ARGS[@]}"
     cp dist/numba_dpex*.whl "${WHEELS_OUTPUT_FOLDER[@]}"
 fi
diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml
index cc826d31b8..a6f3dd98b9 100644
--- a/conda-recipe/meta.yaml
+++ b/conda-recipe/meta.yaml
@@ -1,3 +1,7 @@
+{% set required_compiler_version = "2023.2" %}
+{% set excluded_compiler_version1 = "2024.0.1" %}
+{% set excluded_compiler_version2 = "2024.0.2" %}
+
 package:
     name: numba-dpex
     version: {{ GIT_DESCRIBE_TAG }}
@@ -13,11 +17,16 @@ build:
 requirements:
     build:
         - {{ compiler('cxx') }}
-        - {{ compiler('dpcpp') }}  # [not osx]
+        - {{ compiler('dpcpp') }} >={{ required_compiler_version }},!={{ excluded_compiler_version1 }},!={{ excluded_compiler_version2 }} # [not osx]
+        # specific version of sysroot required by dpcpp, but 2024.0.0 package
+        # does not have it in meta data
+        - sysroot_linux-64 >=2.28  # [linux]
     host:
+        - dpcpp-cpp-rt >={{ required_compiler_version }},!={{ excluded_compiler_version1 }},!={{ excluded_compiler_version2 }}
         - python
         - setuptools >=63.*
         - scikit-build >=0.15*
+        - ninja # [not win]
         - cmake >=3.26*
         - numba >=0.57*
         - dpctl >=0.14*
@@ -25,6 +34,7 @@ requirements:
         - dpcpp-llvm-spirv
         - wheel
     run:
+        - {{ pin_compatible('dpcpp-cpp-rt', min_pin='x.x', max_pin='x') }}
         - python
         - numba >=0.57*
         - dpctl >=0.14*
diff --git a/docs/source/user_guide/kernel_programming/writing_kernels.rst b/docs/source/user_guide/kernel_programming/writing_kernels.rst
index 1e0deae459..5989314aeb 100644
--- a/docs/source/user_guide/kernel_programming/writing_kernels.rst
+++ b/docs/source/user_guide/kernel_programming/writing_kernels.rst
@@ -30,33 +30,6 @@ storing the result of vector summation:
    :name: ex_kernel_declaration_vector_sum
 
 
-.. Kernel Invocation
-.. ------------------
-
-.. When a kernel is launched you must specify the *global size* and the *local size*,
-.. which determine the hierarchy of threads, that is the order in which kernels
-.. will be invoked.
-
-.. The following syntax is used in ``numba-dpex`` for kernel invocation with
-.. specified global and local sizes:
-
-.. ``kernel_function_name[global_size, local_size](kernel arguments)``
-
-.. In the following example we invoke kernel ``kernel_vector_sum`` with global size
-.. specified via variable ``global_size``, and use ``numba_dpex.DEFAULT_LOCAL_SIZE``
-.. constant for setting local size to some default value:
-
-.. .. code-block:: python
-
-..    import numba_dpex as ndpx
-
-..    global_size = 10
-..    kernel_vector_sum[global_size, ndpx.DEFAULT_LOCAL_SIZE](a, b, c)
-
-.. .. note::
-..   Each kernel is compiled once, but it can be called multiple times with different global and local sizes settings.
-
-
 Kernel Invocation
 ------------------
 
diff --git a/docs/source/user_guide/programming_model.rst b/docs/source/user_guide/programming_model.rst
index dac9475bd1..0543036f91 100644
--- a/docs/source/user_guide/programming_model.rst
+++ b/docs/source/user_guide/programming_model.rst
@@ -161,7 +161,7 @@ Specifying the execution queue is done using Python context manager:
    with dpctl.device_context(q):
        # apply the kernel to elements of X, writing value into Y,
        # while executing using given queue
-       numba_dpex_poly[X.size, numba_dpex.DEFAULT_LOCAL_SIZE](X, Y)
+       numba_dpex_poly[numba_dpex.Range(X.size)](X, Y)
 
 The argument to ``device_context`` can be a queue object, a device object for
 which a temporary queue will be created, or a filter selector string. Thus we
diff --git a/environment.yml b/environment.yml
index 6938c9b79f..8d5e3a3105 100644
--- a/environment.yml
+++ b/environment.yml
@@ -9,11 +9,11 @@ channels:
 dependencies:
   - python=3.9
   - gxx_linux-64
-  - dpcpp_linux-64
-  - numba >=0.57*
+  - dpcpp_linux-64>=2023.2,!=2024.0.1,!=2024.0.2
+  - numba ==0.58*
   - dpctl >=0.14*
   - dpnp >=0.11*
-  - mkl >=2021.3.0  # for dpnp
+  - mkl >=2021.3.0 # for dpnp
   - dpcpp-llvm-spirv
   - packaging
   - scikit-build >=0.15*
@@ -21,13 +21,13 @@ dependencies:
   - pytest
   - pip
   - pip:
-    - coverage
-    - pre-commit
-    - flake8
-    - black==20.8b1
-    - pytest-cov
-    - pytest-xdist
-    - pexpect
+      - coverage
+      - pre-commit
+      - flake8
+      - black==20.8b1
+      - pytest-cov
+      - pytest-xdist
+      - pexpect
 variables:
   CHANNELS: -c defaults -c numba -c intel -c numba/label/dev -c dppy/label/dev --override-channels
   CHANNELS_DEV: -c dppy/label/dev -c defaults -c numba -c intel -c numba/label/dev --override-channels
diff --git a/environment/coverage.yml b/environment/coverage.yml
index 8825748540..1b2f310a19 100644
--- a/environment/coverage.yml
+++ b/environment/coverage.yml
@@ -3,14 +3,13 @@ channels:
   - dppy/label/dev
   - numba
   - intel
-  - numba/label/dev
   - conda-forge
   - nodefaults
 dependencies:
   - libffi
   - gxx_linux-64
-  - dpcpp_linux-64
-  - numba=0.57
+  - dpcpp_linux-64>=2023.2,!=2024.0.1,!=2024.0.2
+  - numba==0.58*
   - dpctl
   - dpnp
   - dpcpp-llvm-spirv
diff --git a/environment/docs.yml b/environment/docs.yml
index 8b0a63a1de..4daf1c16b0 100644
--- a/environment/docs.yml
+++ b/environment/docs.yml
@@ -3,14 +3,13 @@ channels:
   - dppy/label/dev
   - numba
   - intel
-  - numba/label/dev
   - conda-forge
   - nodefaults
 dependencies:
   - libffi
   - gxx_linux-64
-  - dpcpp_linux-64
-  - numba=0.57
+  - dpcpp_linux-64>=2023.2,!=2024.0.1,!=2024.0.2
+  - numba==0.58*
   - scikit-build>=0.15*
   - cmake>=3.26*
   - dpctl
diff --git a/environment/pre-commit.yml b/environment/pre-commit.yml
new file mode 100644
index 0000000000..24d420bd2c
--- /dev/null
+++ b/environment/pre-commit.yml
@@ -0,0 +1,23 @@
+name: dev
+channels:
+  - dppy/label/dev
+  - numba
+  - intel
+  - conda-forge
+  - nodefaults
+dependencies:
+  - libffi
+  - numba==0.58*
+  - dpctl
+  - dpnp
+  - dpcpp-llvm-spirv
+  - opencl_rt
+  - coverage
+  - pytest
+  - pytest-cov
+  - pytest-xdist
+  - pexpect
+  - scikit-build>=0.15*
+  - cmake>=3.26*
+  - pre-commit
+  - pylint
diff --git a/numba_dpex/CMakeLists.txt b/numba_dpex/CMakeLists.txt
index 32757ec353..ebcd60fc1e 100644
--- a/numba_dpex/CMakeLists.txt
+++ b/numba_dpex/CMakeLists.txt
@@ -1,48 +1,1 @@
-if(IS_INSTALL)
-    install(DIRECTORY core
-        DESTINATION numba_dpex
-        FILES_MATCHING PATTERN "*.py")
-endif()
-
 add_subdirectory(core/runtime)
-
-if(IS_INSTALL)
-    install(DIRECTORY dpctl_iface
-        DESTINATION numba_dpex
-        FILES_MATCHING PATTERN "*.py")
-endif()
-
-if(IS_INSTALL)
-    install(DIRECTORY dpnp_iface
-        DESTINATION numba_dpex
-        FILES_MATCHING PATTERN "*.py")
-endif()
-
-if(IS_INSTALL)
-    install(DIRECTORY examples
-        DESTINATION numba_dpex)
-endif()
-
-if(IS_INSTALL)
-    install(DIRECTORY numba_patches
-        DESTINATION numba_dpex
-        FILES_MATCHING PATTERN "*.py")
-endif()
-
-if(IS_INSTALL)
-    install(DIRECTORY ocl
-        DESTINATION numba_dpex
-        FILES_MATCHING PATTERN "*.py")
-endif()
-
-if(IS_INSTALL)
-    install(DIRECTORY tests
-        DESTINATION numba_dpex
-        FILES_MATCHING PATTERN "*.py")
-endif()
-
-if(IS_INSTALL)
-    install(DIRECTORY utils
-        DESTINATION numba_dpex
-        FILES_MATCHING PATTERN "*.py")
-endif()
diff --git a/numba_dpex/__init__.py b/numba_dpex/__init__.py
index 381317e800..d754de09b9 100644
--- a/numba_dpex/__init__.py
+++ b/numba_dpex/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -17,18 +17,10 @@
 from numba import __version__ as numba_version
 from numba.np.ufunc.decorators import Vectorize
 
+from numba_dpex.core.kernel_interface.launcher import call_kernel
 from numba_dpex.vectorizers import Vectorize as DpexVectorize
 
-from .numba_patches import (
-    patch_arrayexpr_tree_to_ir,
-    patch_is_ufunc,
-    patch_mk_alloc,
-)
-
-# Monkey patches
-patch_is_ufunc.patch()
-patch_mk_alloc.patch()
-patch_arrayexpr_tree_to_ir.patch()
+from .numba_patches import patch_arrayexpr_tree_to_ir, patch_is_ufunc
 
 
 def load_dpctl_sycl_interface():
@@ -77,12 +69,19 @@ def parse_sem_version(version_string: str) -> Tuple[int, int, int]:
 
 
 numba_sem_version = parse_sem_version(numba_version)
-if numba_sem_version < (0, 57, 0):
+if numba_sem_version < (0, 57, 0) or numba_sem_version >= (0, 59, 0):
     logging.warning(
-        "numba_dpex needs numba 0.57.0, using "
+        "numba_dpex needs at least numba 0.57.0 but no more than 0.59.0, using "
         f"numba={numba_version} may cause unexpected behavior"
     )
 
+# Monkey patches
+patch_is_ufunc.patch()
+if numba_sem_version < (0, 58, 0):
+    from .numba_patches import patch_mk_alloc
+
+    patch_mk_alloc.patch()
+patch_arrayexpr_tree_to_ir.patch()
 
 dpctl_sem_version = parse_sem_version(dpctl.__version__)
 if dpctl_sem_version < (0, 14):
@@ -101,7 +100,7 @@ def parse_sem_version(version_string: str) -> Tuple[int, int, int]:
 
 # Re-export types itself
 import numba_dpex.core.types as types  # noqa E402
-from numba_dpex import config  # noqa E402
+from numba_dpex.core import config  # noqa E402
 from numba_dpex.core.kernel_interface.indexers import (  # noqa E402
     NdRange,
     Range,
@@ -109,35 +108,32 @@ def parse_sem_version(version_string: str) -> Tuple[int, int, int]:
 
 # Re-export all type names
 from numba_dpex.core.types import *  # noqa E402
+from numba_dpex.dpctl_iface import _intrinsic  # noqa E402
 from numba_dpex.dpnp_iface import dpnpimpl  # noqa E402
 
-if config.HAS_NON_HOST_DEVICE:
-    # Re export
-    from .core.targets import dpjit_target, kernel_target
-    from .decorators import dpjit, func, kernel
-    from .ocl.stubs import (
-        GLOBAL_MEM_FENCE,
-        LOCAL_MEM_FENCE,
-        atomic,
-        barrier,
-        get_global_id,
-        get_global_size,
-        get_group_id,
-        get_local_id,
-        get_local_size,
-        get_num_groups,
-        get_work_dim,
-        local,
-        mem_fence,
-        private,
-        sub_group_barrier,
-    )
+from .core.targets import dpjit_target, kernel_target  # noqa E402
+from .decorators import dpjit, func, kernel  # noqa E402
+from .ocl.stubs import (  # noqa E402
+    GLOBAL_MEM_FENCE,
+    LOCAL_MEM_FENCE,
+    atomic,
+    barrier,
+    get_global_id,
+    get_global_size,
+    get_group_id,
+    get_local_id,
+    get_local_size,
+    get_num_groups,
+    get_work_dim,
+    local,
+    mem_fence,
+    private,
+    sub_group_barrier,
+)
+
+load_dpctl_sycl_interface()
+del load_dpctl_sycl_interface
 
-    DEFAULT_LOCAL_SIZE = []
-    load_dpctl_sycl_interface()
-    del load_dpctl_sycl_interface
-else:
-    raise ImportError("No non-host SYCL device found to execute kernels.")
 
 Vectorize.target_registry.ondemand["dpex"] = lambda: DpexVectorize
 
@@ -146,4 +142,4 @@ def parse_sem_version(version_string: str) -> Tuple[int, int, int]:
 __version__ = get_versions()["version"]
 del get_versions
 
-__all__ = types.__all__ + ["Range", "NdRange"]
+__all__ = types.__all__ + ["Range", "NdRange", "call_kernel"]
diff --git a/numba_dpex/_version.py b/numba_dpex/_version.py
index 9d36badc12..0d3376bcee 100644
--- a/numba_dpex/_version.py
+++ b/numba_dpex/_version.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/config.py b/numba_dpex/config.py
deleted file mode 100644
index 855c7748ac..0000000000
--- a/numba_dpex/config.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
-#
-# SPDX-License-Identifier: Apache-2.0
-
-import logging
-import os
-
-import dpctl
-from numba.core import config
-
-
-def _readenv(name, ctor, default):
-    """Original version from numba/core/config.py
-    class _EnvReloader():
-        ...
-        def process_environ():
-            def _readenv(): ...
-    """
-    value = os.environ.get(name)
-    if value is None:
-        return default() if callable(default) else default
-    try:
-        return ctor(value)
-    except Exception:
-        logging.exception(
-            "environ %s defined but failed to parse '%s'" % (name, value)
-        )
-        return default
-
-
-def __getattr__(name):
-    """Fallback to Numba config"""
-    return getattr(config, name)
-
-
-# Ensure dpctl can create a default sycl device.
-# Set this config flag based on if dpctl is found or not.
-# The config flags is used elsewhere inside Numba.
-try:
-    HAS_NON_HOST_DEVICE = dpctl.select_default_device()
-except Exception:
-    logging.exception(
-        "dpctl could not find any non-host SYCL device on the system. "
-        + "A non-host SYCL device is required to use numba_dpex."
-    )
-
-# To save intermediate files generated by th compiler
-SAVE_IR_FILES = _readenv("NUMBA_DPEX_SAVE_IR_FILES", int, 0)
-
-# Turn SPIRV-VALIDATION ON/OFF switch
-SPIRV_VAL = _readenv("NUMBA_DPEX_SPIRV_VAL", int, 0)
-
-# Dump offload diagnostics
-OFFLOAD_DIAGNOSTICS = _readenv("NUMBA_DPEX_OFFLOAD_DIAGNOSTICS", int, 0)
-
-# Emit debug info
-DEBUG = _readenv("NUMBA_DPEX_DEBUG", int, config.DEBUG)
-DEBUGINFO_DEFAULT = _readenv(
-    "NUMBA_DPEX_DEBUGINFO", int, config.DEBUGINFO_DEFAULT
-)
-
-# Emit LLVM assembly language format(.ll)
-DUMP_KERNEL_LLVM = _readenv(
-    "NUMBA_DPEX_DUMP_KERNEL_LLVM", int, config.DUMP_OPTIMIZED
-)
-
-# configs for caching
-# To see the debug messages for the caching.
-# Execute like:
-#   NUMBA_DPEX_DEBUG_CACHE=1 python <code>
-DEBUG_CACHE = _readenv("NUMBA_DPEX_DEBUG_CACHE", int, 0)
-# This is a global flag to turn the caching on/off,
-# regardless of whatever has been specified in Dispatcher.
-# Useful for debugging. Execute like:
-#   NUMBA_DPEX_ENABLE_CACHE=0 python <code>
-# to turn off the caching globally.
-ENABLE_CACHE = _readenv("NUMBA_DPEX_ENABLE_CACHE", int, 1)
-# Capacity of the cache, execute it like:
-#   NUMBA_DPEX_CACHE_SIZE=20 python <code>
-CACHE_SIZE = _readenv("NUMBA_DPEX_CACHE_SIZE", int, 128)
-
-TESTING_SKIP_NO_DPNP = _readenv("NUMBA_DPEX_TESTING_SKIP_NO_DPNP", int, 0)
-TESTING_SKIP_NO_DEBUGGING = _readenv(
-    "NUMBA_DPEX_TESTING_SKIP_NO_DEBUGGING", int, 1
-)
-TESTING_LOG_DEBUGGING = _readenv("NUMBA_DPEX_TESTING_LOG_DEBUGGING", int, DEBUG)
-# Flag to turn on the ConstantSizeStaticLocalMemoryPass in the kernel pipeline.
-# The pass is turned off by default.
-STATIC_LOCAL_MEM_PASS = _readenv("NUMBA_DPEX_STATIC_LOCAL_MEM_PASS", int, 0)
diff --git a/numba_dpex/core/__init__.py b/numba_dpex/core/__init__.py
index b3ea2ab15b..e89664c995 100644
--- a/numba_dpex/core/__init__.py
+++ b/numba_dpex/core/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/caching.py b/numba_dpex/core/caching.py
index d318e9e56c..c636fb7096 100644
--- a/numba_dpex/core/caching.py
+++ b/numba_dpex/core/caching.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -7,7 +7,7 @@
 
 from numba.core.caching import CacheImpl, IndexDataCacheFile
 
-from numba_dpex import config
+from numba_dpex.core import config
 
 
 class _CacheImpl(CacheImpl):
diff --git a/numba_dpex/core/codegen.py b/numba_dpex/core/codegen.py
index 2f86c120fa..f21be104ba 100644
--- a/numba_dpex/core/codegen.py
+++ b/numba_dpex/core/codegen.py
@@ -1,13 +1,15 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
+import warnings
+
 from llvmlite import binding as ll
 from llvmlite import ir as llvmir
 from numba.core import utils
 from numba.core.codegen import CPUCodegen, CPUCodeLibrary
 
-from numba_dpex import config
+from numba_dpex.core import config
 
 SPIR_TRIPLE = {32: " spir-unknown-unknown", 64: "spir64-unknown-unknown"}
 
@@ -27,15 +29,55 @@ class SPIRVCodeLibrary(CPUCodeLibrary):
     def _optimize_functions(self, ll_module):
         pass
 
+    @property
+    def inline_threshold(self):
+        """
+        The inlining threshold value to be used to optimize the final library.
+        """
+        if hasattr(self, "_inline_threshold"):
+            return self._inline_threshold
+        else:
+            return 0
+
+    @inline_threshold.setter
+    def inline_threshold(self, value: int):
+        """Returns the current inlining threshold level for the library."""
+        if value < 0 or value > 3:
+            warnings.warn(
+                "Unsupported inline threshold. Set a value between 0 and 3"
+            )
+            self._inline_threshold = 0
+        else:
+            if value == 3:
+                warnings.warn(
+                    "Due to an existing compiler bug, setting INLINE_THRESHOLD "
+                    f"to {value} can lead to incorrect code generation on "
+                    "certain devices."
+                )
+            self._inline_threshold = value
+
     def _optimize_final_module(self):
         # Run some lightweight optimization to simplify the module.
         pmb = ll.PassManagerBuilder()
 
-        # Make optimization level depending on config.OPT variable
-        pmb.opt_level = config.OPT
+        # Make optimization level depending on config.DPEX_OPT variable
+        pmb.opt_level = config.DPEX_OPT
+        if config.DPEX_OPT > 2:
+            warnings.warn(
+                "Setting NUMBA_DPEX_OPT greater than 2 known to cause issues "
+                + "related to very aggressive optimizations that leads to "
+                + "broken code."
+            )
 
         pmb.disable_unit_at_a_time = False
-        pmb.inlining_threshold = 2
+
+        # The PassManagerBuilder's inlining_threshold property is set only when
+        # inline_threshold is g.t. 0. Doing otherwise, *i.e.*, setting the
+        # pmb.inlining_threshold to 0 will lead to at minimum `alwaysinline`
+        # pass to run.
+        if self.inline_threshold > 0:
+            pmb.inlining_threshold = self.inline_threshold
+
         pmb.disable_unroll_loops = True
         pmb.loop_vectorize = False
         pmb.slp_vectorize = False
@@ -44,6 +86,12 @@ def _optimize_final_module(self):
         pmb.populate(pm)
         pm.run(self._final_module)
 
+    def optimize_final_module(self):
+        """Public member function to optimize the final LLVM module in the
+        library. The function calls the protected overridden function.
+        """
+        self._optimize_final_module()
+
     def _finalize_specific(self):
         # Fix global naming
         for gv in self._final_module.global_variables:
@@ -55,6 +103,10 @@ def get_asm_str(self):
         # generated (in numba_dpex.compiler).
         return None
 
+    @property
+    def final_module(self):
+        return self._final_module
+
 
 class JITSPIRVCodegen(CPUCodegen):
     """
@@ -68,7 +120,7 @@ def _init(self, llvm_module):
         self._data_layout = SPIR_DATA_LAYOUT[utils.MACHINE_BITS]
         self._target_data = ll.create_target_data(self._data_layout)
         self._tm_features = (
-            ""  # We need this for chaching, not sure about this value for now
+            ""  # We need this for caching, not sure about this value for now
         )
 
     def _create_empty_module(self, name):
diff --git a/numba_dpex/core/compiler.py b/numba_dpex/core/compiler.py
index 7c78ae9983..beca01d261 100644
--- a/numba_dpex/core/compiler.py
+++ b/numba_dpex/core/compiler.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -8,7 +8,7 @@
 from numba.core import types as numba_types
 from numba.core.compiler_lock import global_compiler_lock
 
-from numba_dpex import config
+from numba_dpex.core import config
 from numba_dpex.core.exceptions import (
     KernelHasReturnValueError,
     UnreachableError,
diff --git a/numba_dpex/core/config.py b/numba_dpex/core/config.py
new file mode 100644
index 0000000000..500951f068
--- /dev/null
+++ b/numba_dpex/core/config.py
@@ -0,0 +1,101 @@
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import logging
+import os
+
+from numba.core import config
+
+
+def _readenv(name, ctor, default):
+    """Read/write values from/into system environment variable list.
+
+    This function is used to read and write values from (and into) system `env`.
+    This is adapted from `process_environ()` function of `_EnvLoader` class in
+    `numba/core/config.py`.
+
+    Args:
+        name (str): The name of the env variable.
+        ctor (type): The type of the env variable.
+        default (int,float,str): The default value of the env variable.
+
+    Returns:
+        int,float,string: The environment variable value of the specified type.
+    """
+
+    value = os.environ.get(name)
+    if value is None:
+        return default() if callable(default) else default
+    try:
+        return ctor(value)
+    except Exception:
+        logging.exception(
+            "env variable %s defined but failed to parse '%s'" % (name, value)
+        )
+        return default
+
+
+def __getattr__(name):
+    """__getattr__ for numba_dpex's config module.
+
+    This will be used to fallback to numba's config.
+
+    Args:
+        name (str): The name of the env variable.
+
+    Returns:
+        int,float,str: The environment variable value from numba.
+    """
+    return getattr(config, name)
+
+
+# To save intermediate files generated by th compiler
+SAVE_IR_FILES = _readenv("NUMBA_DPEX_SAVE_IR_FILES", int, 0)
+
+# Dump offload diagnostics
+OFFLOAD_DIAGNOSTICS = _readenv("NUMBA_DPEX_OFFLOAD_DIAGNOSTICS", int, 0)
+
+# Emit debug info
+DEBUG = _readenv("NUMBA_DPEX_DEBUG", int, config.DEBUG)
+# The default value for the `debug` flag
+DEBUGINFO_DEFAULT = _readenv(
+    "NUMBA_DPEX_DEBUGINFO", int, config.DEBUGINFO_DEFAULT
+)
+
+# Emit LLVM IR generated for kernel decorated function
+DUMP_KERNEL_LLVM = _readenv("NUMBA_DPEX_DUMP_KERNEL_LLVM", int, 0)
+
+# Emit LLVM module generated to launch a kernel decorated function
+DUMP_KERNEL_LAUNCHER = _readenv("NUMBA_DPEX_DUMP_KERNEL_LAUNCHER", int, 0)
+
+# Enables debug printf messages inside the kernel launcher module generated for
+# a kernel decorated function
+DEBUG_KERNEL_LAUNCHER = _readenv("NUMBA_DPEX_DEBUG_KERNEL_LAUNCHER", int, 0)
+
+# Flag to enable caching, set NUMBA_DPEX_ENABLE_CACHE=0 to turn it off.
+ENABLE_CACHE = _readenv("NUMBA_DPEX_ENABLE_CACHE", int, 1)
+# To specify the default cache size, 20 by default.
+CACHE_SIZE = _readenv("NUMBA_DPEX_CACHE_SIZE", int, 20)
+# Enable debugging of cahcing mechanism, set 1 to turn it on.
+DEBUG_CACHE = _readenv("NUMBA_DPEX_DEBUG_CACHE", int, 0)
+
+# Flag to turn on the ConstantSizeStaticLocalMemoryPass in the kernel pipeline.
+# The pass is turned off by default.
+STATIC_LOCAL_MEM_PASS = _readenv("NUMBA_DPEX_STATIC_LOCAL_MEM_PASS", int, 0)
+
+# Unused flags
+TESTING_SKIP_NO_DPNP = _readenv("NUMBA_DPEX_TESTING_SKIP_NO_DPNP", int, 0)
+TESTING_SKIP_NO_DEBUGGING = _readenv(
+    "NUMBA_DPEX_TESTING_SKIP_NO_DEBUGGING", int, 1
+)
+TESTING_LOG_DEBUGGING = _readenv("NUMBA_DPEX_TESTING_LOG_DEBUGGING", int, DEBUG)
+# Flag to turn on the ConstantSizeStaticLocalMemoryPass in the kernel pipeline.
+# The pass is turned off by default.
+STATIC_LOCAL_MEM_PASS = _readenv("NUMBA_DPEX_STATIC_LOCAL_MEM_PASS", int, 0)
+
+DPEX_OPT = _readenv("NUMBA_DPEX_OPT", int, 2)
+
+INLINE_THRESHOLD = _readenv("NUMBA_DPEX_INLINE_THRESHOLD", int, None)
+
+USE_MLIR = _readenv("NUMBA_DPEX_USE_MLIR", int, 0)
diff --git a/numba_dpex/core/datamodel/__init__.py b/numba_dpex/core/datamodel/__init__.py
index 5985a8ff76..7df6e2020d 100644
--- a/numba_dpex/core/datamodel/__init__.py
+++ b/numba_dpex/core/datamodel/__init__.py
@@ -1,3 +1,3 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/core/datamodel/models.py b/numba_dpex/core/datamodel/models.py
index f3a85e7d07..b3c5ebf71c 100644
--- a/numba_dpex/core/datamodel/models.py
+++ b/numba_dpex/core/datamodel/models.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -9,7 +9,39 @@
 from numba_dpex.core.exceptions import UnreachableError
 from numba_dpex.utils import address_space
 
-from ..types import Array, DpctlSyclQueue, DpnpNdArray, USMNdArray
+from ..types import (
+    Array,
+    DpctlSyclEvent,
+    DpctlSyclQueue,
+    DpnpNdArray,
+    NdRangeType,
+    RangeType,
+    USMNdArray,
+)
+
+
+def _get_flattened_member_count(ty):
+    """Return the number of fields in an instance of a given StructModel."""
+    flattened_member_count = 0
+    members = ty._members
+    for member in members:
+        if isinstance(member, types.UniTuple):
+            flattened_member_count += member.count
+        elif isinstance(
+            member,
+            (
+                types.scalars.Integer,
+                types.misc.PyObject,
+                types.misc.RawPointer,
+                types.misc.CPointer,
+                types.misc.MemInfoPointer,
+            ),
+        ):
+            flattened_member_count += 1
+        else:
+            raise UnreachableError
+
+    return flattened_member_count
 
 
 class GenericPointerModel(PrimitiveModel):
@@ -23,77 +55,62 @@ def __init__(self, dmm, fe_type):
         super(GenericPointerModel, self).__init__(dmm, fe_type, be_type)
 
 
-class USMArrayModel(StructModel):
-    """A data model to represent a Dpex's array types in LLVM IR.
+class USMArrayDeviceModel(StructModel):
+    """A data model to represent a usm array type in the LLVM IR generated for a
+    device-only kernel function.
 
-    Dpex's ArrayModel is based on Numba's ArrayModel for NumPy arrays. The
-    dpex model adds an extra address space attribute to all pointer members
-    in the array.
+    The USMArrayDeviceModel adds an extra address space attribute to the data
+    member. The extra attribute is needed when passing usm_ndarray array
+    arguments to kernels that are compiled for certain OpenCL GPU devices. Note
+    that the address space attribute is applied only to the data member and not
+    other members of USMArrayDeviceModel that are pointers. It is done this way
+    as other pointer members such as meminfo are not used inside a kernel and
+    these members maybe removed from the USMArrayDeviceModel in
+    future (refer #929).
+
+    We use separate data models for host (USMArrayHostModel) and device
+    (USMArrayDeviceModel) as the address space attribute is only required for
+    kernel functions and not needed for functions that are compiled for a host
+    memory space.
     """
 
+    # TODO: Evaluate the need to pass meminfo and parent attributes of an array
+    #   as kernel params: https://github.com/IntelPython/numba-dpex/issues/929
+
     def __init__(self, dmm, fe_type):
         ndim = fe_type.ndim
         members = [
-            (
-                "meminfo",
-                types.CPointer(fe_type.dtype, addrspace=fe_type.addrspace),
-            ),
-            (
-                "parent",
-                types.CPointer(types.pyobject, addrspace=fe_type.addrspace),
-            ),
             ("nitems", types.intp),
             ("itemsize", types.intp),
             (
                 "data",
                 types.CPointer(fe_type.dtype, addrspace=fe_type.addrspace),
             ),
-            (
-                "sycl_queue",
-                types.CPointer(types.void, addrspace=fe_type.addrspace),
-            ),
             ("shape", types.UniTuple(types.intp, ndim)),
             ("strides", types.UniTuple(types.intp, ndim)),
         ]
-        super(USMArrayModel, self).__init__(dmm, fe_type, members)
+        super(USMArrayDeviceModel, self).__init__(dmm, fe_type, members)
 
     @property
     def flattened_field_count(self):
-        """Return the number of fields in an instance of a USMArrayModel."""
-        flattened_member_count = 0
-        members = self._members
-        for member in members:
-            if isinstance(member, types.UniTuple):
-                flattened_member_count += member.count
-            elif isinstance(
-                member,
-                (
-                    types.scalars.Integer,
-                    types.misc.PyObject,
-                    types.misc.RawPointer,
-                    types.misc.CPointer,
-                    types.misc.MemInfoPointer,
-                ),
-            ):
-                flattened_member_count += 1
-            else:
-                raise UnreachableError
-
-        return flattened_member_count
-
-
-class DpnpNdArrayModel(StructModel):
-    """Data model for the DpnpNdArray type.
-
-    DpnpNdArrayModel is used by the numba_dpex.types.DpnpNdArray type and
-    abstracts the usmarystruct_t C type defined in
-    numba_dpex.core.runtime._usmarraystruct.h.
-
-    The DpnpNdArrayModel differs from numba's ArrayModel by including an extra
-    member sycl_queue that maps to _usmarraystruct.sycl_queue pointer. The
+        """
+        Return the number of fields in an instance of a USMArrayDeviceModel.
+        """
+        return _get_flattened_member_count(self)
+
+
+class USMArrayHostModel(StructModel):
+    """Data model for the USMNdArray type when used in a host-only function.
+
+    USMArrayHostModel is used by the numba_dpex.types.USMNdArray and
+    numba_dpex.types.DpnpNdArray type and abstracts the usmarystruct_t C type
+    defined in numba_dpex.core.runtime._usmarraystruct.h.
+
+    The USMArrayDeviceModel differs from numba's ArrayModel by including an
+    extra member sycl_queue that maps to _usmarraystruct.sycl_queue pointer. The
     _usmarraystruct.sycl_queue pointer stores the C++ sycl::queue pointer that
-    was used to allocate the data for the dpnp.ndarray represented by an
-    instance of _usmarraystruct.
+    was used to allocate the data for the dpctl.tensor.usm_ndarray or
+    dpnp.ndarray represented by an instance of _usmarraystruct.
     """
 
     def __init__(self, dmm, fe_type):
@@ -108,31 +125,12 @@ def __init__(self, dmm, fe_type):
             ("shape", types.UniTuple(types.intp, ndim)),
             ("strides", types.UniTuple(types.intp, ndim)),
         ]
-        super(DpnpNdArrayModel, self).__init__(dmm, fe_type, members)
+        super(USMArrayHostModel, self).__init__(dmm, fe_type, members)
 
     @property
     def flattened_field_count(self):
-        """Return the number of fields in an instance of a DpnpNdArrayModel."""
-        flattened_member_count = 0
-        members = self._members
-        for member in members:
-            if isinstance(member, types.UniTuple):
-                flattened_member_count += member.count
-            elif isinstance(
-                member,
-                (
-                    types.scalars.Integer,
-                    types.misc.PyObject,
-                    types.misc.RawPointer,
-                    types.misc.CPointer,
-                    types.misc.MemInfoPointer,
-                ),
-            ):
-                flattened_member_count += 1
-            else:
-                raise UnreachableError
-
-        return flattened_member_count
+        """Return the number of fields in an instance of a USMArrayHostModel."""
+        return _get_flattened_member_count(self)
 
 
 class SyclQueueModel(StructModel):
@@ -150,9 +148,13 @@ class SyclQueueModel(StructModel):
 
     def __init__(self, dmm, fe_type):
         members = [
+            (
+                "meminfo",
+                types.MemInfoPointer(types.pyobject),
+            ),
             (
                 "parent",
-                types.CPointer(types.int8),
+                types.pyobject,
             ),
             (
                 "queue_ref",
@@ -162,6 +164,80 @@ def __init__(self, dmm, fe_type):
         super(SyclQueueModel, self).__init__(dmm, fe_type, members)
 
 
+class SyclEventModel(StructModel):
+    """Represents the native data model for a dpctl.SyclEvent PyObject.
+
+    Numba-dpex uses a C struct as defined in
+    numba_dpex/core/runtime._eventstruct.h to store the required attributes for
+    a ``dpctl.SyclEvent`` Python object.
+
+        - ``event_ref``: An opaque C pointer to an actual SYCL event C++ object.
+        - ``parent``: A PyObject* that stores a reference back to the original
+                      ``dpctl.SyclEvent`` PyObject if the native struct is
+                      created by unboxing the PyObject.
+    """
+
+    def __init__(self, dmm, fe_type):
+        members = [
+            (
+                "meminfo",
+                types.MemInfoPointer(types.pyobject),
+            ),
+            (
+                "parent",
+                types.pyobject,
+            ),
+            (
+                "event_ref",
+                types.CPointer(types.int8),
+            ),
+        ]
+        super(SyclEventModel, self).__init__(dmm, fe_type, members)
+
+
+class RangeModel(StructModel):
+    """The native data model for a
+    numba_dpex.core.kernel_interface.indexers.Range PyObject.
+    """
+
+    def __init__(self, dmm, fe_type):
+        members = [
+            ("ndim", types.int64),
+            ("dim0", types.int64),
+            ("dim1", types.int64),
+            ("dim2", types.int64),
+        ]
+        super(RangeModel, self).__init__(dmm, fe_type, members)
+
+    @property
+    def flattened_field_count(self):
+        """Return the number of fields in an instance of a RangeModel."""
+        return _get_flattened_member_count(self)
+
+
+class NdRangeModel(StructModel):
+    """The native data model for a
+    numba_dpex.core.kernel_interface.indexers.NdRange PyObject.
+    """
+
+    def __init__(self, dmm, fe_type):
+        members = [
+            ("ndim", types.int64),
+            ("gdim0", types.int64),
+            ("gdim1", types.int64),
+            ("gdim2", types.int64),
+            ("ldim0", types.int64),
+            ("ldim1", types.int64),
+            ("ldim2", types.int64),
+        ]
+        super(NdRangeModel, self).__init__(dmm, fe_type, members)
+
+    @property
+    def flattened_field_count(self):
+        """Return the number of fields in an instance of a NdRangeModel."""
+        return _get_flattened_member_count(self)
+
+
 def _init_data_model_manager() -> datamodel.DataModelManager:
     """Initializes a DpexKernelTarget-specific data model manager.
 
@@ -170,12 +246,12 @@ def _init_data_model_manager() -> datamodel.DataModelManager:
     devices, defining a kernel function (spir_kernel calling convention) with
     pointer arguments that have no address space qualifier causes a run time
     crash. For this reason, numba-dpex defines two separate data
-    models: USMArrayModel and DpnpNdArrayModel. When a dpnp.ndarray object is
-    passed as an argument to a ``numba_dpex.kernel`` decorated function it uses
-    the USMArrayModel and when passed to a ``numba_dpex.dpjit`` decorated
-    function it uses the DpnpNdArrayModel. The difference is due to the fact
-    that inside a ``dpjit`` decorated function a dpnp.ndarray object can be
-    passed to any other regular function.
+    models: USMArrayDeviceModel and USMArrayHostModel. When a dpnp.ndarray
+    object is passed as an argument to a ``numba_dpex.kernel`` decorated
+    function it uses the USMArrayDeviceModel and when passed to a
+    ``numba_dpex.dpjit`` decorated function it uses the USMArrayHostModel.
+    The difference is due to the fact that inside a ``dpjit`` decorated function
+    a dpnp.ndarray object can be passed to any other regular function.
 
     Returns:
         DataModelManager: A numba-dpex DpexKernelTarget-specific data model
@@ -183,15 +259,15 @@ def _init_data_model_manager() -> datamodel.DataModelManager:
     """
     dmm = datamodel.default_manager.copy()
     dmm.register(types.CPointer, GenericPointerModel)
-    dmm.register(Array, USMArrayModel)
+    dmm.register(Array, USMArrayDeviceModel)
 
-    # Register the USMNdArray type to USMArrayModel in numba_dpex's data model
-    # manager. The dpex_data_model_manager is used by the DpexKernelTarget
-    dmm.register(USMNdArray, USMArrayModel)
+    # Register the USMNdArray type to USMArrayDeviceModel in numba_dpex's data
+    # model manager. The dpex_data_model_manager is used by the DpexKernelTarget
+    dmm.register(USMNdArray, USMArrayDeviceModel)
 
-    # Register the DpnpNdArray type to USMArrayModel in numba_dpex's data model
-    # manager. The dpex_data_model_manager is used by the DpexKernelTarget
-    dmm.register(DpnpNdArray, USMArrayModel)
+    # Register the DpnpNdArray type to USMArrayDeviceModel in numba_dpex's data
+    # model manager. The dpex_data_model_manager is used by the DpexKernelTarget
+    dmm.register(DpnpNdArray, USMArrayDeviceModel)
 
     # Register the DpctlSyclQueue type to SyclQueueModel in numba_dpex's data
     # model manager. The dpex_data_model_manager is used by the DpexKernelTarget
@@ -203,13 +279,22 @@ def _init_data_model_manager() -> datamodel.DataModelManager:
 dpex_data_model_manager = _init_data_model_manager()
 
 
-# Register the USMNdArray type to USMArrayModel in numba's default data model
-# manager
-register_model(USMNdArray)(USMArrayModel)
+# Register the USMNdArray type to USMArrayDeviceModel in numba's default data
+# model manager
+register_model(USMNdArray)(USMArrayHostModel)
 
-# Register the DpnpNdArray type to DpnpNdArrayModel in numba's default data
+# Register the DpnpNdArray type to USMArrayHostModel in numba's default data
 # model manager
-register_model(DpnpNdArray)(DpnpNdArrayModel)
+register_model(DpnpNdArray)(USMArrayHostModel)
 
 # Register the DpctlSyclQueue type
 register_model(DpctlSyclQueue)(SyclQueueModel)
+
+# Register the DpctlSyclEvent type
+register_model(DpctlSyclEvent)(SyclEventModel)
+
+# Register the RangeType type
+register_model(RangeType)(RangeModel)
+
+# Register the NdRangeType type
+register_model(NdRangeType)(NdRangeModel)
diff --git a/numba_dpex/core/descriptor.py b/numba_dpex/core/descriptor.py
index 0888316f23..2524919053 100644
--- a/numba_dpex/core/descriptor.py
+++ b/numba_dpex/core/descriptor.py
@@ -1,27 +1,78 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 from functools import cached_property
 
-from numba.core import typing
+from numba.core import options, targetconfig, typing
 from numba.core.cpu import CPUTargetOptions
 from numba.core.descriptors import TargetDescriptor
 
-from .targets.dpjit_target import DPEX_TARGET_NAME, DpexTargetContext
+from numba_dpex.core import config
+
+from .targets.dpjit_target import (
+    DPEX_TARGET_NAME,
+    DpexTargetContext,
+    DpexTypingContext,
+)
 from .targets.kernel_target import (
     DPEX_KERNEL_TARGET_NAME,
+    CompilationMode,
     DpexKernelTargetContext,
     DpexKernelTypingContext,
 )
 
+_option_mapping = options._mapping
+
+
+def _inherit_if_not_set(flags, options, name, default=targetconfig._NotSet):
+    if name in options:
+        setattr(flags, name, options[name])
+        return
+
+    cstk = targetconfig.ConfigStack()
+    if cstk:
+        # inherit
+        top = cstk.top()
+        if hasattr(top, name):
+            setattr(flags, name, getattr(top, name))
+            return
+
+    if default is not targetconfig._NotSet:
+        setattr(flags, name, default)
+
+
+class DpexTargetOptions(CPUTargetOptions):
+    experimental = _option_mapping("experimental")
+    release_gil = _option_mapping("release_gil")
+    no_compile = _option_mapping("no_compile")
+    use_mlir = _option_mapping("use_mlir")
+    inline_threshold = _option_mapping("inline_threshold")
+    _compilation_mode = _option_mapping("_compilation_mode")
+
+    def finalize(self, flags, options):
+        super().finalize(flags, options)
+        _inherit_if_not_set(flags, options, "experimental", False)
+        _inherit_if_not_set(flags, options, "release_gil", False)
+        _inherit_if_not_set(flags, options, "no_compile", True)
+        _inherit_if_not_set(flags, options, "use_mlir", False)
+        if config.INLINE_THRESHOLD is not None:
+            _inherit_if_not_set(
+                flags, options, "inline_threshold", config.INLINE_THRESHOLD
+            )
+        else:
+            _inherit_if_not_set(flags, options, "inline_threshold", 0)
+        _inherit_if_not_set(
+            flags, options, "_compilation_mode", CompilationMode.KERNEL
+        )
+
 
 class DpexKernelTarget(TargetDescriptor):
     """
     Implements a target descriptor for numba_dpex.kernel decorated functions.
     """
 
-    options = CPUTargetOptions
+    options = DpexTargetOptions
 
     @cached_property
     def _toplevel_target_context(self):
@@ -63,7 +114,7 @@ def _toplevel_target_context(self):
     @cached_property
     def _toplevel_typing_context(self):
         # Lazily-initialized top-level typing context, for all threads
-        return typing.Context()
+        return DpexTypingContext()
 
     @property
     def target_context(self):
diff --git a/numba_dpex/core/dpjit_dispatcher.py b/numba_dpex/core/dpjit_dispatcher.py
index 74beb37082..f06b9a612f 100644
--- a/numba_dpex/core/dpjit_dispatcher.py
+++ b/numba_dpex/core/dpjit_dispatcher.py
@@ -1,15 +1,45 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-from numba.core import compiler, dispatcher
-from numba.core.target_extension import dispatcher_registry, target_registry
+from numba.core import dispatcher, errors
+from numba.core.target_extension import (
+    dispatcher_registry,
+    target_override,
+    target_registry,
+)
 
+from numba_dpex import numba_sem_version
+from numba_dpex.core.pipelines import dpjit_compiler
 from numba_dpex.core.targets.dpjit_target import DPEX_TARGET_NAME
 
 from .descriptor import dpex_target
 
 
+class _DpjitCompiler(dispatcher._FunctionCompiler):
+    """A special compiler class used to compile numba_dpex.dpjit decorated
+    functions.
+    """
+
+    def _compile_cached(self, args, return_type):
+        # follows the same logic as original one, but triggers _compile_core
+        # with dpex target overload.
+        key = tuple(args), return_type
+        try:
+            return False, self._failed_cache[key]
+        except KeyError:
+            pass
+
+        try:
+            with target_override(DPEX_TARGET_NAME):
+                retval = self._compile_core(args, return_type)
+        except errors.TypingError as e:
+            self._failed_cache[key] = e
+            return False, e
+        else:
+            return True, retval
+
+
 class DpjitDispatcher(dispatcher.Dispatcher):
     """A dpex.djit-specific dispatcher.
 
@@ -26,16 +56,29 @@ def __init__(
         py_func,
         locals={},
         targetoptions={},
-        impl_kind="direct",
-        pipeline_class=compiler.Compiler,
+        pipeline_class=dpjit_compiler.DpjitCompiler,
     ):
-        dispatcher.Dispatcher.__init__(
-            self,
+        if numba_sem_version < (0, 59, 0):
+            super().__init__(
+                py_func=py_func,
+                locals=locals,
+                impl_kind="direct",
+                targetoptions=targetoptions,
+                pipeline_class=pipeline_class,
+            )
+        else:
+            super().__init__(
+                py_func=py_func,
+                locals=locals,
+                targetoptions=targetoptions,
+                pipeline_class=pipeline_class,
+            )
+        self._compiler = _DpjitCompiler(
             py_func,
-            locals=locals,
-            targetoptions=targetoptions,
-            impl_kind=impl_kind,
-            pipeline_class=pipeline_class,
+            self.targetdescr,
+            targetoptions,
+            locals,
+            pipeline_class,
         )
 
 
diff --git a/numba_dpex/core/exceptions.py b/numba_dpex/core/exceptions.py
index 8d5c68366f..d84af98def 100644
--- a/numba_dpex/core/exceptions.py
+++ b/numba_dpex/core/exceptions.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -215,6 +215,12 @@ def __init__(self, kernel_name, *, usmarray_argnum_list) -> None:
                 f"usm_ndarray arguments {usmarray_args} were not allocated "
                 "on the same queue."
             )
+        else:
+            self.message = (
+                f'Execution queue for kernel "{kernel_name}" could '
+                "be deduced using compute follows data programming model. The "
+                "kernel has no USMNdArray argument."
+            )
         super().__init__(self.message)
 
 
@@ -380,3 +386,29 @@ def __init__(self, extra_msg=None) -> None:
         if extra_msg:
             self.message += " due to " + extra_msg
         super().__init__(self.message)
+
+
+class IllegalIntEnumLiteralValueError(Exception):
+    """Exception raised when an IntEnumLiteral is attempted to be created from
+    a non FlagEnum attribute.
+    """
+
+    def __init__(self) -> None:
+        self.message = (
+            "An IntEnumLiteral can only be initialized from a FlagEnum member"
+        )
+        super().__init__(self.message)
+
+
+class InternalError(Exception):
+    """
+    Exception raise when something in the internal compiler machinery failed.
+    """
+
+    def __init__(self, extra_msg=None) -> None:
+        if extra_msg is None:
+            self.message = "An internal compiler error happened and the "
+            "compilation failed."
+        else:
+            self.message = extra_msg
+        super().__init__(self.message)
diff --git a/numba_dpex/core/itanium_mangler.py b/numba_dpex/core/itanium_mangler.py
index 5e8b541274..f0d4c5cf25 100644
--- a/numba_dpex/core/itanium_mangler.py
+++ b/numba_dpex/core/itanium_mangler.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -184,9 +184,6 @@ def mangle_identifier(ident, template_params="", *, abi_tags=(), uid=None):
 
     This treats '.' as '::' in C++.
     """
-    # if uid is not None:
-    # Add uid to abi-tags
-    #    abi_tags = (f"v{uid}", *abi_tags)
     parts = [_len_encoded(_escape_string(x)) for x in ident.split(".")]
     enc_abi_tags = list(map(mangle_abi_tag, abi_tags))
     extras = template_params + "".join(enc_abi_tags)
diff --git a/numba_dpex/core/kernel_interface/__init__.py b/numba_dpex/core/kernel_interface/__init__.py
index c7eb6e81c5..31a84744f4 100644
--- a/numba_dpex/core/kernel_interface/__init__.py
+++ b/numba_dpex/core/kernel_interface/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/kernel_interface/arg_pack_unpacker.py b/numba_dpex/core/kernel_interface/arg_pack_unpacker.py
index bb99e11290..058b9f0701 100644
--- a/numba_dpex/core/kernel_interface/arg_pack_unpacker.py
+++ b/numba_dpex/core/kernel_interface/arg_pack_unpacker.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -47,15 +47,9 @@ def _unpack_usm_array(self, val):
         strides = suai_attrs.strides
         ndim = suai_attrs.dimensions
 
-        # meminfo
-        unpacked_array_attrs.append(ctypes.c_size_t(0))
-        # parent
-        unpacked_array_attrs.append(ctypes.c_size_t(0))
         unpacked_array_attrs.append(ctypes.c_longlong(size))
         unpacked_array_attrs.append(ctypes.c_longlong(itemsize))
         unpacked_array_attrs.append(buf)
-        # queue: unused and passed as void*
-        unpacked_array_attrs.append(ctypes.c_size_t(0))
         for ax in range(ndim):
             unpacked_array_attrs.append(ctypes.c_longlong(shape[ax]))
         for ax in range(ndim):
diff --git a/numba_dpex/core/kernel_interface/arrayobj.py b/numba_dpex/core/kernel_interface/arrayobj.py
new file mode 100644
index 0000000000..8a823a265e
--- /dev/null
+++ b/numba_dpex/core/kernel_interface/arrayobj.py
@@ -0,0 +1,137 @@
+# SPDX-FileCopyrightText: 2012 - 2024 Anaconda Inc.
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: BSD-2-Clause
+
+"""
+This package contains implementation of some numpy.np.arrayobj functions without
+parent and meminfo fields required, because they don't make sense on device.
+These functions intended to be used only in kernel targets like local/private or
+usm array view.
+"""
+
+
+from numba.core import cgutils, types
+from numba.np import arrayobj as np_arrayobj
+
+from numba_dpex.core import types as dpex_types
+
+
+def populate_array(array, data, shape, strides, itemsize):
+    """
+    Helper function for populating array structures.
+    This avoids forgetting to set fields.
+
+    *shape* and *strides* can be Python tuples or LLVM arrays.
+
+    This is analog of numpy.np.arrayobj.populate_array without parent and
+    meminfo fields, because they don't make sense on device. This function
+    intended to be used only in kernel targets.
+    """
+    context = array._context
+    builder = array._builder
+    datamodel = array._datamodel
+    # doesn't matter what this array type instance is, it's just to get the
+    # fields for the datamodel of the standard array type in this context
+    standard_array = dpex_types.Array(types.float64, 1, "C")
+    standard_array_type_datamodel = context.data_model_manager[standard_array]
+    required_fields = set(standard_array_type_datamodel._fields)
+    datamodel_fields = set(datamodel._fields)
+    # Make sure that the presented array object has a data model that is close
+    # enough to an array for this function to proceed.
+    if (required_fields & datamodel_fields) != required_fields:
+        missing = required_fields - datamodel_fields
+        msg = (
+            f"The datamodel for type {array._fe_type} is missing "
+            f"field{'s' if len(missing) > 1 else ''} {missing}."
+        )
+        raise ValueError(msg)
+
+    intp_t = context.get_value_type(types.intp)
+    if isinstance(shape, (tuple, list)):
+        shape = cgutils.pack_array(builder, shape, intp_t)
+    if isinstance(strides, (tuple, list)):
+        strides = cgutils.pack_array(builder, strides, intp_t)
+    if isinstance(itemsize, int):
+        itemsize = intp_t(itemsize)
+
+    attrs = dict(
+        shape=shape,
+        strides=strides,
+        data=data,
+        itemsize=itemsize,
+    )
+
+    # Calc num of items from shape
+    nitems = context.get_constant(types.intp, 1)
+    unpacked_shape = cgutils.unpack_tuple(builder, shape, shape.type.count)
+    # (note empty shape => 0d array therefore nitems = 1)
+    for axlen in unpacked_shape:
+        nitems = builder.mul(nitems, axlen, flags=["nsw"])
+    attrs["nitems"] = nitems
+
+    # Make sure that we have all the fields
+    got_fields = set(attrs.keys())
+    if got_fields != required_fields:
+        raise ValueError("missing {0}".format(required_fields - got_fields))
+
+    # Set field value
+    for k, v in attrs.items():
+        setattr(array, k, v)
+
+    return array
+
+
+def make_view(context, builder, aryty, ary, return_type, data, shapes, strides):
+    """
+    Build a view over the given array with the given parameters.
+
+    This is analog of numpy.np.arrayobj.make_view without parent and
+    meminfo fields, because they don't make sense on device. This function
+    intended to be used only in kernel targets.
+    """
+    retary = np_arrayobj.make_array(return_type)(context, builder)
+    populate_array(
+        retary, data=data, shape=shapes, strides=strides, itemsize=ary.itemsize
+    )
+    return retary
+
+
+def _getitem_array_generic(
+    context, builder, return_type, aryty, ary, index_types, indices
+):
+    """
+    Return the result of indexing *ary* with the given *indices*,
+    returning either a scalar or a view.
+
+    This is analog of numpy.np.arrayobj._getitem_array_generic without parent
+    and meminfo fields, because they don't make sense on device. This function
+    intended to be used only in kernel targets.
+    """
+    dataptr, view_shapes, view_strides = np_arrayobj.basic_indexing(
+        context,
+        builder,
+        aryty,
+        ary,
+        index_types,
+        indices,
+        boundscheck=context.enable_boundscheck,
+    )
+
+    if isinstance(return_type, types.Buffer):
+        # Build array view
+        retary = make_view(
+            context,
+            builder,
+            aryty,
+            ary,
+            return_type,
+            dataptr,
+            view_shapes,
+            view_strides,
+        )
+        return retary._getvalue()
+    else:
+        # Load scalar from 0-d result
+        assert not view_shapes
+        return np_arrayobj.load_item(context, builder, aryty, dataptr)
diff --git a/numba_dpex/core/kernel_interface/dispatcher.py b/numba_dpex/core/kernel_interface/dispatcher.py
index 847e1424cf..d088f8cef8 100644
--- a/numba_dpex/core/kernel_interface/dispatcher.py
+++ b/numba_dpex/core/kernel_interface/dispatcher.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -12,7 +12,8 @@
 from numba.core.types import Array as NpArrayType
 from numba.core.types import void
 
-from numba_dpex import NdRange, Range, config
+from numba_dpex import NdRange, Range
+from numba_dpex.core import config
 from numba_dpex.core.caching import LRUCache, NullCache
 from numba_dpex.core.descriptor import dpex_kernel_target
 from numba_dpex.core.exceptions import (
@@ -94,7 +95,7 @@ def __init__(
             self._kernel_bundle_cache = NullCache()
         self._cache_hits = 0
 
-        if debug_flags or config.OPT == 0:
+        if debug_flags or config.DPEX_OPT == 0:
             # if debug is ON we need to pass additional
             # flags to igc.
             self._create_sycl_kernel_bundle_flags = ["-g", "-cl-opt-disable"]
@@ -293,88 +294,27 @@ def __getitem__(self, args):
 
         """
         if isinstance(args, Range):
-            # we need inversions, see github issue #889
+            # Index inversion is done here as numba-dpex first compiles a native
+            # kernel (OpenCL or Level Zero) and then generates a SYCL
+            # interoperability kernel from it. The convention for unit stride
+            # dimensions is opposite for OpenCL and SYCL
+            # refer: https://registry.khronos.org/SYCL/specs/sycl-2020/html/sycl-2020.html#sec:opencl:kernel-conventions-sycl
+            # For this reason, although numba-dpex follows SYCL like indexing in
+            # the kernel front-end while launching the kernel the indexing is
+            # reversed.
+            #
+            # TODO[1]: It needs to be investigated if we need the index reversal
+            # if we use SYCL-like LLVM IR indexing intrinsic instead of
+            # OpenCL-like LLVM IR intrinsic functions.
+            #
+            # TODO[2]: Do we need to do this when the backend is LevelZero
             self._global_range = list(args)[::-1]
         elif isinstance(args, NdRange):
             # we need inversions, see github issue #889
             self._global_range = list(args.global_range)[::-1]
             self._local_range = list(args.local_range)[::-1]
         else:
-            if (
-                isinstance(args, tuple)
-                and len(args) == 2
-                and isinstance(args[0], int)
-                and isinstance(args[1], int)
-            ):
-                warn(
-                    "Ambiguous kernel launch paramters. If your data have "
-                    + "dimensions > 1, include a default/empty local_range:\n"
-                    + "    <function>[(X,Y), numba_dpex.DEFAULT_LOCAL_RANGE]"
-                    "(<params>)\n"
-                    + "otherwise your code might produce erroneous results.",
-                    DeprecationWarning,
-                    stacklevel=2,
-                )
-                self._global_range = [args[0]]
-                self._local_range = [args[1]]
-                return self
-
-            warn(
-                "The current syntax for specification of kernel launch "
-                + "parameters is deprecated. Users should set the kernel "
-                + "parameters through Range/NdRange classes.\n"
-                + "Example:\n"
-                + "    from numba_dpex import Range,NdRange\n\n"
-                + "    # for global range only\n"
-                + "    <function>[Range(X,Y)](<parameters>)\n"
-                + "    # or,\n"
-                + "    # for both global and local ranges\n"
-                + "    <function>[NdRange((X,Y), (P,Q))](<parameters>)",
-                DeprecationWarning,
-                stacklevel=2,
-            )
-
-            args = [args] if not isinstance(args, Iterable) else args
-            nargs = len(args)
-
-            # Check if the kernel enquing arguments are sane
-            if nargs < 1 or nargs > 2:
-                raise InvalidKernelLaunchArgsError(kernel_name=self.kernel_name)
-
-            g_range = (
-                [args[0]] if not isinstance(args[0], Iterable) else args[0]
-            )
-            # If the optional local size argument is provided
-            l_range = None
-            if nargs == 2:
-                if args[1] != []:
-                    l_range = (
-                        [args[1]]
-                        if not isinstance(args[1], Iterable)
-                        else args[1]
-                    )
-                else:
-                    warn(
-                        "Empty local_range calls are deprecated. Please use "
-                        "Range/NdRange to specify the kernel launch parameters:"
-                        "\n"
-                        + "Example:\n"
-                        + "    from numba_dpex import Range,NdRange\n\n"
-                        + "    # for global range only\n"
-                        + "    <function>[Range(X,Y)](<parameters>)\n"
-                        + "    # or,\n"
-                        + "    # for both global and local ranges\n"
-                        + "    <function>[NdRange((X,Y), (P,Q))](<parameters>)",
-                        DeprecationWarning,
-                        stacklevel=2,
-                    )
-
-            if len(g_range) < 1:
-                raise IllegalRangeValueError(kernel_name=self.kernel_name)
-
-            # we need inversions, see github issue #889
-            self._global_range = list(g_range)[::-1]
-            self._local_range = list(l_range)[::-1] if l_range else None
+            raise InvalidKernelLaunchArgsError(self.kernel_name)
 
         return self
 
@@ -486,6 +426,9 @@ def __call__(self, *args):
         # Make sure the kernel launch range/nd_range are sane
         self._check_ranges(exec_queue.sycl_device)
 
+        # TODO: return event that calls wait if no reference to the object if
+        # it is possible
+        # event = exec_queue.submit( # noqa: E800
         exec_queue.submit(
             sycl_kernel,
             packer.unpacked_args,
diff --git a/numba_dpex/core/kernel_interface/func.py b/numba_dpex/core/kernel_interface/func.py
index 8445c59809..e886071979 100644
--- a/numba_dpex/core/kernel_interface/func.py
+++ b/numba_dpex/core/kernel_interface/func.py
@@ -1,14 +1,11 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-"""_summary_
-"""
-
 from numba.core import sigutils, types
 from numba.core.typing.templates import AbstractTemplate, ConcreteTemplate
 
-from numba_dpex import config
+from numba_dpex.core import config
 from numba_dpex.core.caching import LRUCache, NullCache
 from numba_dpex.core.compiler import compile_with_dpex
 from numba_dpex.core.descriptor import dpex_kernel_target
@@ -67,7 +64,7 @@ def compile(self, arg_types, return_types):
             debug=self._debug,
         )
         func = cres.library.get_function(cres.fndesc.llvm_func_name)
-        cres.target_context.mark_ocl_device(func)
+        cres.target_context.set_spir_func_calling_conv(func)
 
         return cres
 
@@ -159,7 +156,7 @@ def compile(self, args):
                 debug=self._debug,
             )
             func = cres.library.get_function(cres.fndesc.llvm_func_name)
-            cres.target_context.mark_ocl_device(func)
+            cres.target_context.set_spir_func_calling_conv(func)
             libs = [cres.library]
 
             cres.target_context.insert_user_function(self, cres.fndesc, libs)
diff --git a/numba_dpex/core/kernel_interface/indexers.py b/numba_dpex/core/kernel_interface/indexers.py
index 349912ec4e..a5914c14f4 100644
--- a/numba_dpex/core/kernel_interface/indexers.py
+++ b/numba_dpex/core/kernel_interface/indexers.py
@@ -1,9 +1,17 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 from collections.abc import Iterable
 
+from llvmlite import ir as llvmir
+from numba.core import cgutils, errors, types
+from numba.core.datamodel import default_manager
+from numba.extending import intrinsic, overload
+
+# can't import name because of the circular import
+DPEX_TARGET_NAME = "dpex"
+
 
 class Range(tuple):
     """A data structure to encapsulate a single kernel launch parameter.
@@ -18,6 +26,8 @@ class Range(tuple):
     the behavior of `sycl::range`.
     """
 
+    UNDEFINED_DIMENSION = -1
+
     def __new__(cls, dim0, dim1=None, dim2=None):
         """Constructs a 1, 2, or 3 dimensional range.
 
@@ -74,6 +84,50 @@ def size(self):
         else:
             return self[0]
 
+    @property
+    def ndim(self) -> int:
+        """Returns the rank of a Range object.
+
+        Returns:
+            int: Number of dimensions in the Range object
+        """
+        return len(self)
+
+    @property
+    def dim0(self) -> int:
+        """Return the extent of the first dimension for the Range object.
+
+        Returns:
+            int: Extent of first dimension for the Range object
+        """
+        return self[0]
+
+    @property
+    def dim1(self) -> int:
+        """Return the extent of the second dimension for the Range object.
+
+        Returns:
+            int: Extent of second dimension for the Range object or -1 for 1D
+            Range
+        """
+        try:
+            return self[1]
+        except IndexError:
+            return Range.UNDEFINED_DIMENSION
+
+    @property
+    def dim2(self) -> int:
+        """Return the extent of the second dimension for the Range object.
+
+        Returns:
+            int: Extent of second dimension for the Range object or -1 for 1D or
+            2D Range
+        """
+        try:
+            return self[2]
+        except IndexError:
+            return Range.UNDEFINED_DIMENSION
+
 
 class NdRange:
     """A class to encapsulate all kernel launch parameters.
@@ -169,3 +223,170 @@ def __repr__(self):
             str: str representation for NdRange class.
         """
         return self.__str__()
+
+    def __eq__(self, other):
+        if isinstance(other, NdRange):
+            return (
+                self.global_range == other.global_range
+                and self.local_range == other.local_range
+            )
+        else:
+            return False
+
+
+@intrinsic(target=DPEX_TARGET_NAME)
+def _intrin_range_alloc(typingctx, ty_dim0, ty_dim1, ty_dim2, ty_range):
+    ty_retty = ty_range.instance_type
+    sig = ty_retty(
+        ty_dim0,
+        ty_dim1,
+        ty_dim2,
+        ty_range,
+    )
+
+    def codegen(context, builder, sig, args):
+        typ = sig.return_type
+        dim0, dim1, dim2, _ = args
+        range_struct = cgutils.create_struct_proxy(typ)(context, builder)
+        range_struct.dim0 = dim0
+
+        if not isinstance(sig.args[1], types.NoneType):
+            range_struct.dim1 = dim1
+        else:
+            range_struct.dim1 = llvmir.Constant(
+                llvmir.types.IntType(64), Range.UNDEFINED_DIMENSION
+            )
+
+        if not isinstance(sig.args[2], types.NoneType):
+            range_struct.dim2 = dim2
+        else:
+            range_struct.dim2 = llvmir.Constant(
+                llvmir.types.IntType(64), Range.UNDEFINED_DIMENSION
+            )
+
+        range_struct.ndim = llvmir.Constant(llvmir.types.IntType(64), typ.ndim)
+
+        return range_struct._getvalue()
+
+    return sig, codegen
+
+
+@intrinsic(target=DPEX_TARGET_NAME)
+def _intrin_ndrange_alloc(
+    typingctx, ty_global_range, ty_local_range, ty_ndrange
+):
+    ty_retty = ty_ndrange.instance_type
+    sig = ty_retty(
+        ty_global_range,
+        ty_local_range,
+        ty_ndrange,
+    )
+    range_datamodel = default_manager.lookup(ty_global_range)
+
+    def codegen(context, builder, sig, args):
+        typ = sig.return_type
+
+        global_range, local_range, _ = args
+        ndrange_struct = cgutils.create_struct_proxy(typ)(context, builder)
+        ndrange_struct.ndim = llvmir.Constant(
+            llvmir.types.IntType(64), typ.ndim
+        )
+        ndrange_struct.gdim0 = builder.extract_value(
+            global_range,
+            range_datamodel.get_field_position("dim0"),
+        )
+        ndrange_struct.gdim1 = builder.extract_value(
+            global_range,
+            range_datamodel.get_field_position("dim1"),
+        )
+        ndrange_struct.gdim2 = builder.extract_value(
+            global_range,
+            range_datamodel.get_field_position("dim2"),
+        )
+        ndrange_struct.ldim0 = builder.extract_value(
+            local_range,
+            range_datamodel.get_field_position("dim0"),
+        )
+        ndrange_struct.ldim1 = builder.extract_value(
+            local_range,
+            range_datamodel.get_field_position("dim1"),
+        )
+        ndrange_struct.ldim2 = builder.extract_value(
+            local_range,
+            range_datamodel.get_field_position("dim2"),
+        )
+
+        return ndrange_struct._getvalue()
+
+    return sig, codegen
+
+
+@overload(Range, target=DPEX_TARGET_NAME)
+def _ol_range_init(dim0, dim1=None, dim2=None):
+    """Numba overload of the Range constructor to make it usable inside an
+    njit and dpjit decorated function.
+
+    """
+    from numba_dpex.core.types import RangeType
+
+    ndims = 1
+    ty_optional_dims = (dim1, dim2)
+
+    # A Range should at least have the 0th dimension populated
+    if not isinstance(dim0, types.Integer):
+        raise errors.TypingError(
+            "Expected a Range's dimension should to be an Integer value, but "
+            "encountered " + dim0.name
+        )
+
+    for ty_dim in ty_optional_dims:
+        if isinstance(ty_dim, types.Integer):
+            ndims += 1
+        elif ty_dim is not None:
+            raise errors.TypingError(
+                "Expected a Range's dimension to be an Integer value, "
+                f"but {type(ty_dim)} was provided."
+            )
+
+    ret_ty = RangeType(ndims)
+
+    def impl(dim0, dim1=None, dim2=None):
+        return _intrin_range_alloc(dim0, dim1, dim2, ret_ty)
+
+    return impl
+
+
+@overload(NdRange, target=DPEX_TARGET_NAME)
+def _ol_ndrange_init(global_range, local_range):
+    """Numba overload of the NdRange constructor to make it usable inside an
+    njit and dpjit decorated function.
+
+    """
+    from numba_dpex.core.exceptions import UnmatchedNumberOfRangeDimsError
+    from numba_dpex.core.types import NdRangeType, RangeType
+
+    if not isinstance(global_range, RangeType):
+        raise errors.TypingError(
+            "Only global range values specified as a Range are "
+            "supported inside dpjit"
+        )
+
+    if not isinstance(local_range, RangeType):
+        raise errors.TypingError(
+            "Only local range values specified as a Range are "
+            "supported inside dpjit"
+        )
+
+    if not global_range.ndim == local_range.ndim:
+        raise UnmatchedNumberOfRangeDimsError(
+            kernel_name="",
+            global_ndims=global_range.ndim,
+            local_ndims=local_range.ndim,
+        )
+
+    ret_ty = NdRangeType(global_range.ndim)
+
+    def impl(global_range, local_range):
+        return _intrin_ndrange_alloc(global_range, local_range, ret_ty)
+
+    return impl
diff --git a/numba_dpex/core/kernel_interface/kernel_base.py b/numba_dpex/core/kernel_interface/kernel_base.py
index f068d338e7..f0fb24e67f 100644
--- a/numba_dpex/core/kernel_interface/kernel_base.py
+++ b/numba_dpex/core/kernel_interface/kernel_base.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/kernel_interface/launcher.py b/numba_dpex/core/kernel_interface/launcher.py
new file mode 100644
index 0000000000..6c9b0ac782
--- /dev/null
+++ b/numba_dpex/core/kernel_interface/launcher.py
@@ -0,0 +1,22 @@
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Launcher package to provide the same way of calling kernel as experimental
+one."""
+
+
+def call_kernel(kernel_fn, index_space, *kernel_args) -> None:
+    """Syntax sugar for calling kernel the same way as experimental one.
+    It is a temporary glue for the experimental kernel migration.
+
+    Args:
+        kernel_fn (numba_dpex.experimental.KernelDispatcher): A
+        numba_dpex.kernel decorated function that is compiled to a
+        KernelDispatcher by numba_dpex.
+        index_space (Range | NdRange): A numba_dpex.Range or numba_dpex.NdRange
+        type object that specifies the index space for the kernel.
+        kernel_args : List of objects that are passed to the numba_dpex.kernel
+        decorated function.
+    """
+    kernel_fn[index_space](*kernel_args)
diff --git a/numba_dpex/core/kernel_interface/spirv_kernel.py b/numba_dpex/core/kernel_interface/spirv_kernel.py
index 9a5a306d90..a7008c6528 100644
--- a/numba_dpex/core/kernel_interface/spirv_kernel.py
+++ b/numba_dpex/core/kernel_interface/spirv_kernel.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -7,9 +7,11 @@
 
 from numba.core import ir
 
-from numba_dpex import config, spirv_generator
+from numba_dpex import spirv_generator
+from numba_dpex.core import config
 from numba_dpex.core.compiler import compile_with_dpex
 from numba_dpex.core.exceptions import UncompiledKernelError, UnreachableError
+from numba_dpex.core.targets.kernel_target import DpexKernelTargetContext
 
 from .kernel_base import KernelInterface
 
@@ -133,9 +135,17 @@ def compile(
         )
 
         func = cres.library.get_function(cres.fndesc.llvm_func_name)
-        kernel = cres.target_context.prepare_ocl_kernel(
-            func, cres.signature.args
-        )
+        kernel_targetctx: DpexKernelTargetContext = cres.target_context
+        kernel = kernel_targetctx.prepare_spir_kernel(func, cres.signature.args)
+
+        # XXX: Setting the inline_threshold in the following way is a temporary
+        # workaround till the JitKernel dispatcher is replaced by
+        # experimental.dispatcher.KernelDispatcher.
+        if config.INLINE_THRESHOLD is not None:
+            cres.library.inline_threshold = config.INLINE_THRESHOLD
+        else:
+            cres.library.inline_threshold = 0
+
         cres.library._optimize_final_module()
         self._llvm_module = kernel.module.__str__()
         self._module_name = kernel.name
diff --git a/numba_dpex/core/kernel_interface/utils.py b/numba_dpex/core/kernel_interface/utils.py
index d16b3db1b7..014274cf58 100644
--- a/numba_dpex/core/kernel_interface/utils.py
+++ b/numba_dpex/core/kernel_interface/utils.py
@@ -1,8 +1,7 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-
 from numba_dpex.core.exceptions import ExecutionQueueInferenceError
 from numba_dpex.core.types import USMNdArray
 
diff --git a/numba_dpex/core/parfors/__init__.py b/numba_dpex/core/parfors/__init__.py
index 00fec5f515..7206ff64a6 100644
--- a/numba_dpex/core/parfors/__init__.py
+++ b/numba_dpex/core/parfors/__init__.py
@@ -1,3 +1,3 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/core/parfors/kernel_builder.py b/numba_dpex/core/parfors/kernel_builder.py
index 349e9a984b..588a617684 100644
--- a/numba_dpex/core/parfors/kernel_builder.py
+++ b/numba_dpex/core/parfors/kernel_builder.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -25,7 +25,7 @@
 from numba.parfors import parfor
 
 import numba_dpex as dpex
-from numba_dpex import config
+from numba_dpex.core import config
 
 from ..descriptor import dpex_kernel_target
 from ..types import DpnpNdArray, USMNdArray
@@ -40,7 +40,7 @@ def __init__(
         signature,
         kernel_args,
         kernel_arg_types,
-        queue,
+        queue: dpctl.SyclQueue,
     ):
         self.name = name
         self.kernel = kernel
@@ -80,7 +80,7 @@ def _compile_kernel_parfor(
     )
 
     dpctl_create_program_from_spirv_flags = []
-    if debug or config.OPT == 0:
+    if debug or config.DPEX_OPT == 0:
         # if debug is ON we need to pass additional flags to igc.
         dpctl_create_program_from_spirv_flags = ["-g", "-cl-opt-disable"]
 
@@ -244,7 +244,7 @@ def create_kernel_for_parfor(
     has_aliases,
     races,
     parfor_outputs,
-):
+) -> ParforKernel:
     """
     Creates a numba_dpex.kernel function for a parfor node.
 
@@ -422,7 +422,7 @@ def create_kernel_for_parfor(
     # arrays are on same device. We can take the queue from the first input
     # array and use that to compile the kernel.
 
-    exec_queue = None
+    exec_queue: dpctl.SyclQueue = None
 
     for arg in parfor_args:
         obj = typemap[arg]
diff --git a/numba_dpex/core/parfors/parfor_lowerer.py b/numba_dpex/core/parfors/parfor_lowerer.py
index 07fbe50ba3..cd9ecd6d8b 100644
--- a/numba_dpex/core/parfors/parfor_lowerer.py
+++ b/numba_dpex/core/parfors/parfor_lowerer.py
@@ -1,33 +1,36 @@
-# SPDX-FileCopyrightText: 2020 - 2022 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 import copy
+from collections import namedtuple
 
 from llvmlite import ir as llvmir
-from numba.core import ir, types
+from numba.core import cgutils, ir, types
 from numba.parfors.parfor import (
     find_potential_aliases_parfor,
     get_parfor_outputs,
 )
 
-from numba_dpex import config
+from numba_dpex.core import config
+from numba_dpex.core.datamodel.models import (
+    dpex_data_model_manager as kernel_dmm,
+)
 from numba_dpex.core.parfors.reduction_helper import (
     ReductionHelper,
     ReductionKernelVariables,
 )
 from numba_dpex.core.utils.kernel_launcher import KernelLaunchIRBuilder
+from numba_dpex.dpctl_iface import libsyclinterface_bindings as sycl
 
 from ..exceptions import UnsupportedParforError
 from ..types.dpnp_ndarray_type import DpnpNdArray
-from .kernel_builder import create_kernel_for_parfor
+from .kernel_builder import ParforKernel, create_kernel_for_parfor
 from .reduction_kernel_builder import (
     create_reduction_main_kernel_for_parfor,
     create_reduction_remainder_kernel_for_parfor,
 )
 
-from numba_dpex.core.datamodel.models import dpex_data_model_manager as dpex_dmm
-
 # A global list of kernels to keep the objects alive indefinitely.
 keep_alive_kernels = []
 
@@ -62,11 +65,8 @@ def _getvar(lowerer, x):
         var_val = lowerer.varmap[x]
 
     if var_val:
-        if not isinstance(var_val.type, llvmir.PointerType):
-            with lowerer.builder.goto_entry_block():
-                var_val_ptr = lowerer.builder.alloca(var_val.type)
-            lowerer.builder.store(var_val, var_val_ptr)
-            return var_val_ptr
+        if isinstance(var_val.type, llvmir.PointerType):
+            return lowerer.builder.load(var_val)
         else:
             return var_val
     else:
@@ -85,115 +85,15 @@ class ParforLowerImpl:
     for a parfor and submits it to a queue.
     """
 
-    def _get_exec_queue(self, kernel_fn, lowerer):
-        """Creates a stack variable storing the sycl queue pointer used to
-        launch the kernel function.
-        """
-        self.kernel_builder = KernelLaunchIRBuilder(lowerer, kernel_fn.kernel)
-
-        # Create a local variable storing a pointer to a DPCTLSyclQueueRef
-        # pointer.
-        self.curr_queue = self.kernel_builder.get_queue(
-            exec_queue=kernel_fn.queue
-        )
-
-    def _build_kernel_arglist(self, kernel_fn, lowerer):
-        """Creates local variables for all the arguments and the argument types
-        that are passes to the kernel function.
-
-        Args:
-            kernel_fn: Kernel function to be launched.
-            lowerer: The Numba lowerer used to generate the LLVM IR
-
-        Raises:
-            AssertionError: If the LLVM IR Value for an argument defined in
-            Numba IR is not found.
-        """
-        num_flattened_args = 0
-
-        # Compute number of args to be passed to the kernel. Note that the
-        # actual number of kernel arguments is greater than the count of
-        # kernel_fn.kernel_args as arrays get flattened.
-        for arg_type in kernel_fn.kernel_arg_types:
-            if isinstance(arg_type, DpnpNdArray):
-                datamodel = dpex_dmm.lookup(arg_type)
-                num_flattened_args += datamodel.flattened_field_count
-            elif arg_type == types.complex64 or arg_type == types.complex128:
-                num_flattened_args += 2
-            else:
-                num_flattened_args += 1
-
-        # Create LLVM values for the kernel args list and kernel arg types list
-        self.args_list = self.kernel_builder.allocate_kernel_arg_array(
-            num_flattened_args
-        )
-        self.args_ty_list = self.kernel_builder.allocate_kernel_arg_ty_array(
-            num_flattened_args
-        )
-        # Populate the args_list and the args_ty_list LLVM arrays
-        self.kernel_arg_num = 0
-        for arg_num, arg in enumerate(kernel_fn.kernel_args):
-            argtype = kernel_fn.kernel_arg_types[arg_num]
-            llvm_val = _getvar(lowerer, arg)
-            if isinstance(argtype, DpnpNdArray):
-                datamodel = dpex_dmm.lookup(argtype)
-                self.kernel_builder.build_array_arg(
-                    array_val=llvm_val,
-                    array_data_model=datamodel,
-                    array_rank=argtype.ndim,
-                    arg_list=self.args_list,
-                    args_ty_list=self.args_ty_list,
-                    arg_num=self.kernel_arg_num,
-                )
-                self.kernel_arg_num += datamodel.flattened_field_count
-            else:
-                if argtype == types.complex64:
-                    self.kernel_builder.build_complex_arg(
-                        llvm_val,
-                        types.float32,
-                        self.args_list,
-                        self.args_ty_list,
-                        self.kernel_arg_num,
-                    )
-                    self.kernel_arg_num += 2
-                elif argtype == types.complex128:
-                    self.kernel_builder.build_complex_arg(
-                        llvm_val,
-                        types.float64,
-                        self.args_list,
-                        self.args_ty_list,
-                        self.kernel_arg_num,
-                    )
-                    self.kernel_arg_num += 2
-                else:
-                    self.kernel_builder.build_arg(
-                        llvm_val,
-                        argtype,
-                        self.args_list,
-                        self.args_ty_list,
-                        self.kernel_arg_num,
-                    )
-                    self.kernel_arg_num += 1
-
-    def _submit_parfor_kernel(
+    def _loop_ranges(
         self,
         lowerer,
-        kernel_fn,
         loop_ranges,
     ):
-        """
-        Adds a call to submit a kernel function into the function body of the
-        current Numba JIT compiled function.
-        """
-        # Ensure that the Python arguments are kept alive for the duration of
-        # the kernel execution
-        keep_alive_kernels.append(kernel_fn.kernel)
-
-        self._get_exec_queue(kernel_fn, lowerer)
-        self._build_kernel_arglist(kernel_fn, lowerer)
         # Create a global range over which to submit the kernel based on the
         # loop_ranges of the parfor
         global_range = []
+
         # SYCL ranges can have at max 3 dimension. If the parfor is of a higher
         # dimension then the indexing for the higher dimensions is done inside
         # the kernel.
@@ -207,38 +107,19 @@ def _submit_parfor_kernel(
                     "non-unit strides are not yet supported."
                 )
             global_range.append(stop)
-
+        # For now the local_range is always an empty list as numba_dpex always
+        # submits kernels generated for parfor nodes as range kernels.
+        # The provision is kept here if in future there is newer functionality
+        # to submit these kernels as ndrange.
         local_range = []
 
-        # Submit a synchronous kernel
-        self.kernel_builder.submit_sync_kernel(
-            self.curr_queue,
-            self.kernel_arg_num,
-            self.args_list,
-            self.args_ty_list,
-            global_range,
-            local_range,
-        )
-
-        # At this point we can free the DPCTLSyclQueueRef (curr_queue)
-        self.kernel_builder.free_queue(sycl_queue_val=self.curr_queue)
+        return global_range, local_range
 
-    def _submit_reduction_main_parfor_kernel(
+    def _reduction_ranges(
         self,
         lowerer,
-        kernel_fn,
         reductionHelper=None,
     ):
-        """
-        Adds a call to submit the main kernel of a parfor reduction into the
-        function body of the current Numba JIT compiled function.
-        """
-        # Ensure that the Python arguments are kept alive for the duration of
-        # the kernel execution
-        keep_alive_kernels.append(kernel_fn.kernel)
-
-        self._get_exec_queue(kernel_fn, lowerer)
-        self._build_kernel_arglist(kernel_fn, lowerer)
         # Create a global range over which to submit the kernel based on the
         # loop_ranges of the parfor
         global_range = []
@@ -252,50 +133,63 @@ def _submit_reduction_main_parfor_kernel(
             _load_range(lowerer, reductionHelper.work_group_size)
         )
 
-        # Submit a synchronous kernel
-        self.kernel_builder.submit_sync_kernel(
-            self.curr_queue,
-            self.kernel_arg_num,
-            self.args_list,
-            self.args_ty_list,
-            global_range,
-            local_range,
-        )
+        return global_range, local_range
+
+    def _remainder_ranges(self, lowerer):
+        # Create a global range over which to submit the kernel based on the
+        # loop_ranges of the parfor
+        global_range = []
+
+        stop = _load_range(lowerer, 1)
+
+        global_range.append(stop)
 
-    def _submit_reduction_remainder_parfor_kernel(
+        local_range = []
+
+        return global_range, local_range
+
+    def _submit_parfor_kernel(
         self,
         lowerer,
-        kernel_fn,
+        kernel_fn: ParforKernel,
+        global_range,
+        local_range,
     ):
         """
-        Adds a call to submit the remainder kernel of a parfor reduction into
-        the function body of the current Numba JIT compiled function.
+        Adds a call to submit a kernel function into the function body of the
+        current Numba JIT compiled function.
         """
         # Ensure that the Python arguments are kept alive for the duration of
         # the kernel execution
         keep_alive_kernels.append(kernel_fn.kernel)
+        kl_builder = KernelLaunchIRBuilder(
+            lowerer.context, lowerer.builder, kernel_dmm
+        )
 
-        self._get_exec_queue(kernel_fn, lowerer)
-        self._build_kernel_arglist(kernel_fn, lowerer)
-        # Create a global range over which to submit the kernel based on the
-        # loop_ranges of the parfor
-        global_range = []
-
-        stop = _load_range(lowerer, 1)
+        queue_ref = kl_builder.get_queue(exec_queue=kernel_fn.queue)
 
-        global_range.append(stop)
+        kernel_args = []
+        for arg in kernel_fn.kernel_args:
+            kernel_args.append(_getvar(lowerer, arg))
 
-        local_range = []
+        kernel_ref_addr = kernel_fn.kernel.addressof_ref()
+        kernel_ref = lowerer.builder.inttoptr(
+            lowerer.context.get_constant(types.uintp, kernel_ref_addr),
+            cgutils.voidptr_t,
+        )
 
-        # Submit a synchronous kernel
-        self.kernel_builder.submit_sync_kernel(
-            self.curr_queue,
-            self.kernel_arg_num,
-            self.args_list,
-            self.args_ty_list,
-            global_range,
-            local_range,
+        kl_builder.set_kernel(kernel_ref)
+        kl_builder.set_queue(queue_ref)
+        kl_builder.set_range(global_range, local_range)
+        kl_builder.set_arguments(
+            kernel_fn.kernel_arg_types, kernel_args=kernel_args
         )
+        kl_builder.set_dependent_events([])
+        event_ref = kl_builder.submit()
+
+        sycl.dpctl_event_wait(lowerer.builder, event_ref)
+        sycl.dpctl_event_delete(lowerer.builder, event_ref)
+        sycl.dpctl_queue_delete(lowerer.builder, queue_ref)
 
     def _reduction_codegen(
         self,
@@ -359,10 +253,15 @@ def _reduction_codegen(
             parfor_reddict,
         )
 
-        self._submit_reduction_main_parfor_kernel(
+        global_range, local_range = self._reduction_ranges(
+            lowerer, reductionHelperList[0]
+        )
+
+        self._submit_parfor_kernel(
             lowerer,
             parfor_kernel,
-            reductionHelperList[0],
+            global_range,
+            local_range,
         )
 
         parfor_kernel = create_reduction_remainder_kernel_for_parfor(
@@ -375,9 +274,13 @@ def _reduction_codegen(
             reductionHelperList,
         )
 
-        self._submit_reduction_remainder_parfor_kernel(
+        global_range, local_range = self._remainder_ranges(lowerer)
+
+        self._submit_parfor_kernel(
             lowerer,
             parfor_kernel,
+            global_range,
+            local_range,
         )
 
         reductionKernelVar.copy_final_sum_to_host(parfor_kernel)
@@ -446,7 +349,7 @@ def _lower_parfor_as_kernel(self, lowerer, parfor):
         flags.error_model = "numpy"
 
         # Can't get here unless
-        # flags.set('auto_parallel', ParallelOptions(True))
+        # flags.set('auto_parallel', ParallelOptions(True)) # noqa: E800
         index_var_typ = typemap[parfor.loop_nests[0].index_variable.name]
 
         # index variables should have the same type, check rest of indices
@@ -491,11 +394,14 @@ def _lower_parfor_as_kernel(self, lowerer, parfor):
                 # FIXME: Make the exception more informative
                 raise UnsupportedParforError
 
+            global_range, local_range = self._loop_ranges(lowerer, loop_ranges)
+
             # Finally submit the kernel
             self._submit_parfor_kernel(
                 lowerer,
                 parfor_kernel,
-                loop_ranges,
+                global_range,
+                local_range,
             )
 
         # TODO: free the kernel at this point
diff --git a/numba_dpex/core/parfors/reduction_helper.py b/numba_dpex/core/parfors/reduction_helper.py
index cb8471ee19..047eeb2b11 100644
--- a/numba_dpex/core/parfors/reduction_helper.py
+++ b/numba_dpex/core/parfors/reduction_helper.py
@@ -1,6 +1,7 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
+
 import copy
 import operator
 
@@ -17,8 +18,11 @@
 from numba.parfors.parfor_lowering_utils import ParforLoweringBuilder
 
 from numba_dpex import utils
+from numba_dpex.core.datamodel.models import (
+    dpex_data_model_manager as kernel_dmm,
+)
 from numba_dpex.core.utils.kernel_launcher import KernelLaunchIRBuilder
-from numba_dpex.dpctl_iface import DpctlCAPIFnBuilder
+from numba_dpex.dpctl_iface import libsyclinterface_bindings as sycl
 
 from ..types.dpnp_ndarray_type import DpnpNdArray
 
@@ -41,7 +45,7 @@ def _allocate_partial_reduction_arrays(
         # Get the type of the reduction variable.
         redvar_typ = lowerer.fndesc.typemap[red_name]
 
-        # redarrvar_typ is type(partial_sum)
+        # redarrvar_typ is type(partial_sum) # noqa: E800 help understanding
         redarrvar_typ = self._redtyp_to_redarraytype(redvar_typ, inputArrayType)
         reddtype = redarrvar_typ.dtype
         redarrdim = redarrvar_typ.ndim
@@ -63,7 +67,7 @@ def _allocate_partial_reduction_arrays(
             name="tot_work",
         )
 
-        # global_size_mod = tot_work%work_group_size
+        # global_size_mod = tot_work%work_group_size # noqa: E800 help understanding
         ir_expr = ir.Expr.binop(
             operator.mod, total_work_var, work_group_size_var, loc
         )
@@ -232,7 +236,7 @@ def __init__(
         loop_body = copy.copy(parfor_node.loop_body)
         remove_dels(loop_body)
 
-        # parfor_dim = len(parfor_node.loop_nests)
+        # parfor_dim = len(parfor_node.loop_nests) # noqa: E800 help understanding
         loop_indices = [
             loop_nest.index_variable.name
             for loop_nest in parfor_node.loop_nests
@@ -393,27 +397,19 @@ def lowerer(self):
     def work_group_size(self):
         return self._work_group_size
 
-    def copy_final_sum_to_host(self, psrfor_kernel):
+    def copy_final_sum_to_host(self, parfor_kernel):
         lowerer = self.lowerer
-        ir_builder = KernelLaunchIRBuilder(lowerer, psrfor_kernel.kernel)
+        kl_builder = KernelLaunchIRBuilder(
+            lowerer.context, lowerer.builder, kernel_dmm
+        )
 
         # Create a local variable storing a pointer to a DPCTLSyclQueueRef
         # pointer.
-        curr_queue = ir_builder.get_queue(exec_queue=psrfor_kernel.queue)
+        queue_ref = kl_builder.get_queue(exec_queue=parfor_kernel.queue)
 
         builder = lowerer.builder
         context = lowerer.context
 
-        memcpy_fn = DpctlCAPIFnBuilder.get_dpctl_queue_memcpy(
-            builder=builder, context=context
-        )
-        event_del_fn = DpctlCAPIFnBuilder.get_dpctl_event_delete(
-            builder=builder, context=context
-        )
-        event_wait_fn = DpctlCAPIFnBuilder.get_dpctl_event_wait(
-            builder=builder, context=context
-        )
-
         for i, redvar in enumerate(self.parfor_redvars):
             srcVar = self.final_sum_names[i]
 
@@ -443,15 +439,14 @@ def copy_final_sum_to_host(self, psrfor_kernel):
             )
 
             args = [
-                builder.load(curr_queue),
+                queue_ref,
                 dest,
                 src,
                 builder.load(item_size),
             ]
 
-            event_ref = builder.call(memcpy_fn, args)
-
-            builder.call(event_wait_fn, [event_ref])
-            builder.call(event_del_fn, [event_ref])
+            event_ref = sycl.dpctl_queue_memcpy(builder, *args)
+            sycl.dpctl_event_wait(builder, event_ref)
+            sycl.dpctl_event_delete(builder, event_ref)
 
-        ir_builder.free_queue(sycl_queue_val=curr_queue)
+        sycl.dpctl_queue_delete(builder, queue_ref)
diff --git a/numba_dpex/core/parfors/reduction_kernel_builder.py b/numba_dpex/core/parfors/reduction_kernel_builder.py
index 15ad116288..a73f838710 100644
--- a/numba_dpex/core/parfors/reduction_kernel_builder.py
+++ b/numba_dpex/core/parfors/reduction_kernel_builder.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/passes/__init__.py b/numba_dpex/core/passes/__init__.py
index d090fb56f5..39317b336f 100644
--- a/numba_dpex/core/passes/__init__.py
+++ b/numba_dpex/core/passes/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/passes/dufunc_inliner.py b/numba_dpex/core/passes/dufunc_inliner.py
index d59ebf98f7..74947513d4 100644
--- a/numba_dpex/core/passes/dufunc_inliner.py
+++ b/numba_dpex/core/passes/dufunc_inliner.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/passes/parfor_legalize_cfd_pass.py b/numba_dpex/core/passes/parfor_legalize_cfd_pass.py
index 6c5a10760b..b7b9402b66 100644
--- a/numba_dpex/core/passes/parfor_legalize_cfd_pass.py
+++ b/numba_dpex/core/passes/parfor_legalize_cfd_pass.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -42,6 +42,8 @@ class ParforLegalizeCFDPassImpl:
 
     """
 
+    # TODO: fix point algorithm implementation
+
     inputUsmTypeStrToInt = {"device": 3, "shared": 2, "host": 1}
     inputUsmTypeIntToStr = {3: "device", 2: "shared", 1: "host"}
 
@@ -59,7 +61,14 @@ def _check_if_dpnp_empty_call(self, call_stmt, block):
             and isinstance(func_def.value, ir.Expr)
             and func_def.value.op == "getattr"
         ):
-            raise AssertionError
+            # TODO: write unit test that this check passes for the instruction
+            # generated by dpnp.empty() after type inferring pass
+            # Possible implementation:
+            # 1. dpjit with dpnp.empty()
+            # 2. generate Numba IR after type inference for it by using dummy
+            #    pipeline
+            # 3. check that we have assertion making place
+            return False
 
         module_name = block.find_variable_assignment(
             func_def.value.list_vars()[0].name
@@ -162,6 +171,7 @@ def _legalize_parfor_params(self, parfor):
             str: The device filter string for the parfor if the parfor is
             compute follows data conforming.
         """
+        # TODO: check if attribute is dpnp specific
         if parfor.params is None:
             return
 
@@ -205,9 +215,11 @@ def _legalize_cfd_parfor_blocks(self, parfor):
         """Legalize the parfor params based on the compute follows data
         programming model and usm allocator precedence rule.
         """
+        # this function just sets queue for dpnp and empty attribute for it
         conforming_device_ty = self._legalize_parfor_params(parfor)
 
         # Update the parfor's lowerer attribute
+        # this sets if we are in dpnp or numpy parfor
         parfor.lowerer = ParforLowerFactory.get_lowerer(conforming_device_ty)
 
         init_block = parfor.init_block
@@ -221,6 +233,7 @@ def _legalize_cfd_parfor_blocks(self, parfor):
                 self._legalize_stmt(stmt, block, inparfor=True)
 
     def _legalize_expr(self, stmt, lhs, lhsty, parent_block, inparfor=False):
+        # TODO: rename to infer queue
         rhs = stmt.value
         if rhs.op == "call":
             if self._check_if_dpnp_empty_call(rhs.func, parent_block):
@@ -305,6 +318,7 @@ def run(self):
 
 @register_pass(mutates_CFG=True, analysis_only=False)
 class ParforLegalizeCFDPass(FunctionPass):
+    # TODO: rename to execution queue inferring Pass
     _name = "parfor_Legalize_CFD_pass"
 
     def __init__(self):
diff --git a/numba_dpex/core/passes/passes.py b/numba_dpex/core/passes/passes.py
index 1bc1ab42aa..792618883f 100644
--- a/numba_dpex/core/passes/passes.py
+++ b/numba_dpex/core/passes/passes.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -13,7 +13,7 @@
 from numba.core.ir_utils import remove_dels
 from numba.core.typed_passes import NativeLowering
 
-from numba_dpex import config
+from numba_dpex.core import config
 
 
 @register_pass(mutates_CFG=True, analysis_only=False)
diff --git a/numba_dpex/core/pipelines/__init__.py b/numba_dpex/core/pipelines/__init__.py
index 00fec5f515..7206ff64a6 100644
--- a/numba_dpex/core/pipelines/__init__.py
+++ b/numba_dpex/core/pipelines/__init__.py
@@ -1,3 +1,3 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/core/pipelines/dpjit_compiler.py b/numba_dpex/core/pipelines/dpjit_compiler.py
index f84469d528..d4c63ee056 100644
--- a/numba_dpex/core/pipelines/dpjit_compiler.py
+++ b/numba_dpex/core/pipelines/dpjit_compiler.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2022 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -37,6 +37,8 @@ class _DpjitPassBuilder(object):
     execution.
     """
 
+    _use_mlir = False
+
     @staticmethod
     def define_typed_pipeline(state, name="dpex_dpjit_typed"):
         """Returns the typed part of the nopython pipeline"""
@@ -55,7 +57,8 @@ def define_typed_pipeline(state, name="dpex_dpjit_typed"):
             pm.add_pass(NopythonRewrites, "nopython rewrites")
         pm.add_pass(ParforPass, "convert to parfors")
         pm.add_pass(
-            ParforLegalizeCFDPass, "Legalize parfors for compute follows data"
+            ParforLegalizeCFDPass,
+            "Legalize parfors for compute follows data",
         )
         pm.add_pass(ParforFusionPass, "fuse parfors")
         pm.add_pass(ParforPreLoweringPass, "parfor prelowering")
@@ -63,11 +66,22 @@ def define_typed_pipeline(state, name="dpex_dpjit_typed"):
         pm.finalize()
         return pm
 
-    @staticmethod
-    def define_nopython_lowering_pipeline(state, name="dpex_dpjit_lowering"):
+    @classmethod
+    def define_nopython_lowering_pipeline(
+        cls, state, name="dpex_dpjit_lowering"
+    ):
         """Returns an nopython mode pipeline based PassManager"""
         pm = PassManager(name)
 
+        flags = state.flags
+        if cls._use_mlir or hasattr(flags, "use_mlir") and flags.use_mlir:
+            from numba_mlir.mlir.passes import MlirReplaceParfors
+
+            pm.add_pass(
+                MlirReplaceParfors,
+                "Lower parfor using MLIR pipeline",
+            )
+
         # legalize
         pm.add_pass(
             NoPythonSupportedFeatureValidation,
@@ -85,11 +99,11 @@ def define_nopython_lowering_pipeline(state, name="dpex_dpjit_lowering"):
         pm.finalize()
         return pm
 
-    @staticmethod
-    def define_nopython_pipeline(state, name="dpex_dpjit_nopython"):
+    @classmethod
+    def define_nopython_pipeline(cls, state, name="dpex_dpjit_nopython"):
         """Returns an nopython mode pipeline based PassManager"""
         # compose pipeline from untyped, typed and lowering parts
-        dpb = _DpjitPassBuilder
+        dpb = cls
         pm = PassManager(name)
         untyped_passes = DefaultPassBuilder.define_untyped_pipeline(state)
         pm.passes.extend(untyped_passes.passes)
@@ -104,9 +118,15 @@ def define_nopython_pipeline(state, name="dpex_dpjit_nopython"):
         return pm
 
 
+class _DpjitPassBuilderMlir(_DpjitPassBuilder):
+    _use_mlir = True
+
+
 class DpjitCompiler(CompilerBase):
     """Dpex's compiler pipeline to offload parfor nodes into SYCL kernels."""
 
+    _pass_builder = _DpjitPassBuilder
+
     def define_pipelines(self):
         pms = []
         self.state.parfor_diagnostics = ExtendedParforDiagnostics()
@@ -114,7 +134,15 @@ def define_pipelines(self):
             "parfor_diagnostics"
         ] = self.state.parfor_diagnostics
         if not self.state.flags.force_pyobject:
-            pms.append(_DpjitPassBuilder.define_nopython_pipeline(self.state))
+            pms.append(self._pass_builder.define_nopython_pipeline(self.state))
         if self.state.status.can_fallback or self.state.flags.force_pyobject:
             raise UnsupportedCompilationModeError()
         return pms
+
+
+class DpjitCompilerMlir(DpjitCompiler):
+    _pass_builder = _DpjitPassBuilderMlir
+
+
+def get_compiler(use_mlir):
+    return DpjitCompilerMlir if use_mlir else DpjitCompiler
diff --git a/numba_dpex/core/pipelines/kernel_compiler.py b/numba_dpex/core/pipelines/kernel_compiler.py
index 5a1cf01375..45e212ccd5 100644
--- a/numba_dpex/core/pipelines/kernel_compiler.py
+++ b/numba_dpex/core/pipelines/kernel_compiler.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -29,7 +29,7 @@
     WithLifting,
 )
 
-from numba_dpex import config
+from numba_dpex.core import config
 from numba_dpex.core.exceptions import UnsupportedCompilationModeError
 from numba_dpex.core.passes.passes import (
     ConstantSizeStaticLocalMemoryPass,
@@ -140,7 +140,6 @@ def define_nopython_lowering_pipeline(state, name="dpex_kernel_lowering"):
         )
         pm.add_pass(IRLegalization, "ensure IR is legal prior to lowering")
 
-        # lower
         # NativeLowering has some issue with freevar ambiguity,
         # therefore, we are using QualNameDisambiguationLowering instead
         # numba-dpex github issue: https://github.com/IntelPython/numba-dpex/issues/898
@@ -173,7 +172,7 @@ def define_nopython_pipeline(state, name="dpex_kernel_nopython"):
 
 
 class KernelCompiler(CompilerBase):
-    """Dpex's  kernel compilation pipeline."""
+    """Dpex's kernel compilation pipeline."""
 
     def define_pipelines(self):
         pms = []
@@ -181,4 +180,14 @@ def define_pipelines(self):
             pms.append(_KernelPassBuilder.define_nopython_pipeline(self.state))
         if self.state.status.can_fallback or self.state.flags.force_pyobject:
             raise UnsupportedCompilationModeError()
+
+        # Compile the kernel without generating a cpython or a cfunc wrapper
+        self.state.flags.no_cpython_wrapper = True
+        self.state.flags.no_cfunc_wrapper = True
+        # The pass pipeline does not generate an executable when compiling a
+        # kernel function. Instead, the
+        # kernel_dispatcher._KernelCompiler.compile generates the executable in
+        # the form of a host callable launcher function
+        self.state.flags.no_compile = True
+
         return pms
diff --git a/numba_dpex/core/runtime/CMakeLists.txt b/numba_dpex/core/runtime/CMakeLists.txt
index 674cc03db6..d12d2d08ad 100644
--- a/numba_dpex/core/runtime/CMakeLists.txt
+++ b/numba_dpex/core/runtime/CMakeLists.txt
@@ -24,6 +24,10 @@ project(_dpexrt_python
     DESCRIPTION "A Python C extension for numba-dpex runtime."
 )
 
+# Help conda build find path from both host and build env.
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
+
 # Get numba include path
 if(NOT DEFINED Numba_INCLUDE_DIRS)
     execute_process(
@@ -81,18 +85,20 @@ message(STATUS "CMAKE_MODULE_PATH=" "${CMAKE_MODULE_PATH}")
 find_package(Python 3.9 REQUIRED
   COMPONENTS Interpreter Development.Module NumPy)
 find_package(Dpctl REQUIRED)
-find_package(NumPy REQUIRED)
+find_package(IntelSYCL REQUIRED)
 
 # Includes
 include(GNUInstallDirs)
 include_directories(${Python_INCLUDE_DIRS})
-include_directories(${NumPy_INCLUDE_DIRS})
+include_directories(${Python_NumPy_INCLUDE_DIRS})
 include_directories(${Numba_INCLUDE_DIRS})
 include_directories(${Dpctl_INCLUDE_DIRS})
 include_directories(.)
 
 # Source files, *.c
-file(GLOB SOURCES "*.c")
+file(GLOB_RECURSE DPEXRT_SOURCES CONFIGURE_DEPENDS "*.c")
+file(GLOB_RECURSE KERNEL_SOURCES CONFIGURE_DEPENDS "*.cpp")
+set(SOURCES ${DPEXRT_SOURCES} ${KERNEL_SOURCES})
 
 # Link dpctl library path with -L
 link_directories(${DPCTL_LIBRARY_PATH})
@@ -100,20 +106,12 @@ link_directories(${DPCTL_LIBRARY_PATH})
 # Output static library, *.so or *.dll
 python_add_library(${PROJECT_NAME} MODULE ${SOURCES})
 
+# Add SYCL to target, this must come after python_add_library()
+add_sycl_to_target(TARGET ${PROJECT_NAME} SOURCES ${KERNEL_SOURCES})
+
 # Link the DPCTLSyclInterface library to target
 target_link_libraries(${PROJECT_NAME} PRIVATE DPCTLSyclInterface)
 
-# If IS_DEVELOP, copy back the target into numba_dpex/core/runtime
-if(IS_DEVELOP)
-    add_custom_command(
-        TARGET ${PROJECT_NAME}
-        POST_BUILD
-        COMMAND ${CMAKE_COMMAND} -E copy
-        ${CMAKE_BINARY_DIR}/numba_dpex/core/runtime/*.so
-        ${CMAKE_SOURCE_DIR}/numba_dpex/core/runtime/
-    )
-endif()
-
 # Install
 install(
     TARGETS ${PROJECT_NAME} LIBRARY DESTINATION numba_dpex/core/runtime
diff --git a/numba_dpex/core/runtime/__init__.py b/numba_dpex/core/runtime/__init__.py
index 05bb42d2f4..6984574044 100644
--- a/numba_dpex/core/runtime/__init__.py
+++ b/numba_dpex/core/runtime/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2021 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2021 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/runtime/_dbg_printer.h b/numba_dpex/core/runtime/_dbg_printer.h
index 6dea2dcdde..809230a2b7 100644
--- a/numba_dpex/core/runtime/_dbg_printer.h
+++ b/numba_dpex/core/runtime/_dbg_printer.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2023 Intel Corporation
+// SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/runtime/_dpexrt_python.c b/numba_dpex/core/runtime/_dpexrt_python.c
index b2340b799d..10e441ed6c 100644
--- a/numba_dpex/core/runtime/_dpexrt_python.c
+++ b/numba_dpex/core/runtime/_dpexrt_python.c
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -20,9 +20,14 @@
 #include "_nrt_python_helper.h"
 
 #include "_dbg_printer.h"
+#include "_eventstruct.h"
 #include "_queuestruct.h"
 #include "_usmarraystruct.h"
 
+#include "experimental/kernel_caching.h"
+#include "experimental/nrt_reserve_meminfo.h"
+#include "numba/core/runtime/nrt_external.h"
+
 // forward declarations
 static struct PyUSMArrayObject *PyUSMNdArray_ARRAYOBJ(PyObject *obj);
 static npy_intp product_of_shape(npy_intp *shape, npy_intp ndim);
@@ -61,9 +66,19 @@ DPEXRT_sycl_usm_ndarray_to_python_acqref(usmarystruct_t *arystruct,
                                          int ndim,
                                          int writeable,
                                          PyArray_Descr *descr);
-static int DPEXRT_sycl_queue_from_python(PyObject *obj,
+static int DPEXRT_sycl_queue_from_python(NRT_api_functions *nrt,
+                                         PyObject *obj,
                                          queuestruct_t *queue_struct);
-static PyObject *DPEXRT_sycl_queue_to_python(queuestruct_t *queuestruct);
+static int DPEXRT_sycl_event_from_python(NRT_api_functions *nrt,
+                                         PyObject *obj,
+                                         eventstruct_t *event_struct);
+static PyObject *DPEXRT_sycl_queue_to_python(NRT_api_functions *nrt,
+                                             queuestruct_t *queuestruct);
+static PyObject *DPEXRT_sycl_event_to_python(NRT_api_functions *nrt,
+                                             eventstruct_t *eventstruct);
+static int DPEXRT_sycl_event_init(NRT_api_functions *nrt,
+                                  DPCTLSyclEventRef event,
+                                  eventstruct_t *eventstruct);
 
 /** An NRT_external_malloc_func implementation using DPCTLmalloc_device.
  *
@@ -457,9 +472,9 @@ static NRT_MemInfo *NRT_MemInfo_new_from_usmndarray(PyObject *ndarrobj,
     mi->size = nitems * itemsize;
     mi->external_allocator = ext_alloca;
 
-    DPEXRT_DEBUG(drt_debug_print(
-        "DPEXRT-DEBUG: NRT_MemInfo_init mi=%p external_allocator=%p\n", mi,
-        ext_alloca));
+    DPEXRT_DEBUG(drt_debug_print("DPEXRT-DEBUG: NRT_MemInfo_init mi=%p "
+                                 "external_allocator=%p at %s, line %d\n",
+                                 mi, ext_alloca, __FILE__, __LINE__));
 
     return mi;
 
@@ -725,12 +740,19 @@ static struct PyUSMArrayObject *PyUSMNdArray_ARRAYOBJ(PyObject *obj)
 {
     PyObject *arrayobj = NULL;
 
-    arrayobj = PyObject_GetAttrString(obj, "_array_obj");
+    if (PyObject_TypeCheck(obj, &PyUSMArrayType)) {
+        DPEXRT_DEBUG(
+            drt_debug_print("DPEXRT-DEBUG: usm array was passed directly\n"));
+        arrayobj = obj;
+    }
+    else if (PyObject_HasAttrString(obj, "_array_obj")) {
+        arrayobj = PyObject_GetAttrString(obj, "_array_obj");
 
-    if (!arrayobj)
-        return NULL;
-    if (!PyObject_TypeCheck(arrayobj, &PyUSMArrayType))
-        return NULL;
+        if (!arrayobj)
+            return NULL;
+        if (!PyObject_TypeCheck(arrayobj, &PyUSMArrayType))
+            return NULL;
+    }
 
     struct PyUSMArrayObject *pyusmarrayobj =
         (struct PyUSMArrayObject *)(arrayobj);
@@ -783,17 +805,19 @@ static int DPEXRT_sycl_usm_ndarray_from_python(PyObject *obj,
 
     // Increment the ref count on obj to prevent CPython from garbage
     // collecting the array.
+    // TODO: add extra description why do we need this
     Py_IncRef(obj);
 
     DPEXRT_DEBUG(drt_debug_print(
-        "DPEXRT-DEBUG: In DPEXRT_sycl_usm_ndarray_from_python.\n"));
+        "DPEXRT-DEBUG: In DPEXRT_sycl_usm_ndarray_from_python at %s, line %d\n",
+        __FILE__, __LINE__));
 
     // Check if the PyObject obj has an _array_obj attribute that is of
     // dpctl.tensor.usm_ndarray type.
     if (!(arrayobj = PyUSMNdArray_ARRAYOBJ(obj))) {
         DPEXRT_DEBUG(drt_debug_print(
-            "DPEXRT-ERROR: PyUSMNdArray_ARRAYOBJ check failed %d\n", __FILE__,
-            __LINE__));
+            "DPEXRT-ERROR: PyUSMNdArray_ARRAYOBJ check failed at %s, line %d\n",
+            __FILE__, __LINE__));
         goto error;
     }
 
@@ -842,13 +866,14 @@ static int DPEXRT_sycl_usm_ndarray_from_python(PyObject *obj,
     for (i = 0; i < ndim; ++i, ++p)
         *p = shape[i];
 
-    // DPCTL returns a NULL pointer if the array is contiguous. dpctl stores
-    // strides as number of elements and Numba stores strides as bytes, for
-    // that reason we are multiplying stride by itemsize when unboxing the
-    // external array.
-
-    // FIXME: Stride computation should check order and adjust how strides are
-    // calculated. Right now strides are assuming that order is C contigous.
+    // dpctl returns a NULL pointer for the stride vector if the array has a
+    // C-contiguous layout. For all other cases, including strided views and
+    // F contiguous layouts, the actual stride values are returned. Also, dpctl
+    // stores strides as number of elements and Numba follows NumPy and stores
+    // strides as number of bytes. For that reason, we multiply strides by
+    // itemsize when unboxing an usm_ndarray if dpctl returned a popualted
+    // stride vector. For the default C contiguous case, strides are
+    // calculated directly as number of bytes based on itemsize.
     if (strides) {
         for (i = 0; i < ndim; ++i, ++p) {
             *p = strides[i] << exp;
@@ -863,6 +888,11 @@ static int DPEXRT_sycl_usm_ndarray_from_python(PyObject *obj,
         }
     }
 
+    DPEXRT_DEBUG(
+        drt_debug_print("DPEXRT-DEBUG: Done with unboxing call to "
+                        "DPEXRT_sycl_usm_ndarray_from_python at %s, line %d\n",
+                        __FILE__, __LINE__));
+
     return 0;
 
 error:
@@ -897,7 +927,7 @@ static PyObject *box_from_arystruct_parent(usmarystruct_t *arystruct,
                                            int ndim,
                                            PyArray_Descr *descr)
 {
-    int i = 0, exp = 0;
+    int i = 0, j = 0, k = 0, exp = 0;
     npy_intp *p = NULL;
     npy_intp *shape = NULL, *strides = NULL;
     PyObject *array = arystruct->parent;
@@ -929,6 +959,8 @@ static PyObject *box_from_arystruct_parent(usmarystruct_t *arystruct,
     shape = UsmNDArray_GetShape(arrayobj);
     strides = UsmNDArray_GetStrides(arrayobj);
 
+    // Ensure the shape of the array to be boxed matches the shape of the
+    // original parent.
     for (i = 0; i < ndim; i++, p++) {
         if (shape[i] != *p)
             return NULL;
@@ -938,20 +970,52 @@ static PyObject *box_from_arystruct_parent(usmarystruct_t *arystruct,
     itemsize = arystruct->itemsize;
     while (itemsize >>= 1)
         exp++;
-    // dpctl stores strides as number of elements and Numba stores strides as
+
+    // Ensure the strides of the array to be boxed matches the shape of the
+    // original parent. Things to note:
+    //
+    // 1. dpctl only stores stride information if the array has a non-unit
+    // stride. If the array is unit strided then dpctl does not populate the
+    // stride attribute. To verify strides, we compute the strides from the
+    // shape vector.
+    //
+    // 2. dpctl stores strides as number of elements and Numba stores strides as
     // bytes, for that reason we are multiplying stride by itemsize when
-    // unboxing the external array.
+    // unboxing the external array and dividing by itemsize when boxing the
+    // array back.
+
     if (strides) {
-        if (strides[i] << exp != *p)
-            return NULL;
+        for (i = 0; i < ndim; ++i, ++p) {
+            if (strides[i] << exp != *p) {
+                DPEXRT_DEBUG(
+                    drt_debug_print("DPEXRT-DEBUG: Arrayobj cannot be boxed "
+                                    "from parent as strides in the "
+                                    "arystruct are not the same as "
+                                    "the strides in the parent object. "
+                                    "Expected stride = %d actual stride = %d\n",
+                                    strides[i] << exp, *p));
+                return NULL;
+            }
+        }
     }
     else {
-        for (i = 1; i < ndim; ++i, ++p) {
-            if (shape[i] != *p)
+        npy_intp tmp;
+        for (i = (ndim * 2) - 1; i >= ndim; --i, ++p) {
+            tmp = 1;
+            for (j = i, k = ndim - 1; j > ndim; --j, --k)
+                tmp *= shape[k];
+            tmp <<= exp;
+            if (tmp != *p) {
+                DPEXRT_DEBUG(
+                    drt_debug_print("DPEXRT-DEBUG: Arrayobj cannot be boxed "
+                                    "from parent as strides in the "
+                                    "arystruct are not the same as "
+                                    "the strides in the parent object. "
+                                    "Expected stride = %d actual stride = %d\n",
+                                    tmp, *p));
                 return NULL;
+            }
         }
-        if (*p != 1)
-            return NULL;
     }
 
     // At the end of boxing our Meminfo destructor gets called and that will
@@ -1108,6 +1172,8 @@ DPEXRT_sycl_usm_ndarray_to_python_acqref(usmarystruct_t *arystruct,
         return MOD_ERROR_VAL;
     }
 
+    // TODO: check if the object is dpctl tensor and return usm_ndarr_obj then
+
     //  call new on dpnp_array
     dpnp_array_mod = PyImport_ImportModule("dpnp.dpnp_array");
     if (!dpnp_array_mod) {
@@ -1173,17 +1239,12 @@ DPEXRT_sycl_usm_ndarray_to_python_acqref(usmarystruct_t *arystruct,
  *                          represent a dpctl.SyclQueue inside Numba.
  * @return   {return}       Return code indicating success (0) or failure (-1).
  */
-static int DPEXRT_sycl_queue_from_python(PyObject *obj,
+static int DPEXRT_sycl_queue_from_python(NRT_api_functions *nrt,
+                                         PyObject *obj,
                                          queuestruct_t *queue_struct)
 {
-
     struct PySyclQueueObject *queue_obj = NULL;
     DPCTLSyclQueueRef queue_ref = NULL;
-    PyGILState_STATE gstate;
-
-    // Increment the ref count on obj to prevent CPython from garbage
-    // collecting the dpctl.SyclQueue object
-    Py_IncRef(obj);
 
     // We are unconditionally casting obj to a struct PySyclQueueObject*. If
     // the obj is not a struct PySyclQueueObject* then the SyclQueue_GetQueueRef
@@ -1209,7 +1270,14 @@ static int DPEXRT_sycl_queue_from_python(PyObject *obj,
                                  DPCTLDeviceMgr_GetDeviceInfoStr(device_ref));
                  DPCTLDevice_Delete(device_ref););
 
-    queue_struct->parent = obj;
+    // We are doing incref here to ensure python does not release the object
+    // while NRT references it. Coresponding decref is called by NRT in
+    // NRT_MemInfo_pyobject_dtor once there is no reference to this object by
+    // the code managed by NRT.
+    Py_INCREF(queue_obj);
+    queue_struct->meminfo =
+        nrt->manage_memory(queue_obj, NRT_MemInfo_pyobject_dtor);
+    queue_struct->parent = (PyObject *)queue_obj;
     queue_struct->queue_ref = queue_ref;
 
     return 0;
@@ -1223,11 +1291,6 @@ static int DPEXRT_sycl_queue_from_python(PyObject *obj,
         "DPEXRT-ERROR: Failed to unbox dpctl SyclQueue into a Numba "
         "queuestruct at %s, line %d\n",
         __FILE__, __LINE__));
-    gstate = PyGILState_Ensure();
-    // decref the python object
-    Py_DECREF(obj);
-    // release the GIL
-    PyGILState_Release(gstate);
 
     return -1;
 }
@@ -1243,30 +1306,159 @@ static int DPEXRT_sycl_queue_from_python(PyObject *obj,
  * @return   {return}       A PyObject created from the queuestruct->parent, if
  *                          the PyObject could not be created return NULL.
  */
-static PyObject *DPEXRT_sycl_queue_to_python(queuestruct_t *queuestruct)
+static PyObject *DPEXRT_sycl_queue_to_python(NRT_api_functions *nrt,
+                                             queuestruct_t *queuestruct)
 {
-    PyObject *orig_queue = NULL;
-    PyGILState_STATE gstate;
+    PyObject *queue_obj = queuestruct->parent;
 
-    orig_queue = queuestruct->parent;
-    // FIXME: Better error checking is needed to enforce the boxing of the queue
-    // object. For now, only the minimal is done as the returning of SyclQueue
-    // from a dpjit function should not be a used often and the dpctl C API for
-    // type checking etc. is not ready.
-    if (orig_queue == NULL) {
-        PyErr_Format(PyExc_ValueError,
-                     "In 'box_from_queuestruct_parent', "
-                     "failed to create a new dpctl.SyclQueue object.");
-        return NULL;
+    if (queue_obj == NULL) {
+        // Make create copy of queue_ref so we don't need to manage nrt lifetime
+        // from python object.
+        queue_obj = (PyObject *)SyclQueue_Make(queuestruct->queue_ref);
+    }
+    else {
+        // Unfortunately we can not optimize (nrt->release that triggers
+        // Py_DECREF() from the destructor) and Py_INCREF() because nrt may need
+        // the object even if we return it to python.
+        // We need to increase reference count because we are returning new
+        // reference to the same queue.
+        Py_INCREF(queue_obj);
     }
 
-    gstate = PyGILState_Ensure();
-    // decref the parent python object as we did an incref when unboxing it
-    Py_DECREF(orig_queue);
-    // release the GIL
-    PyGILState_Release(gstate);
+    // We need to release meminfo since we are taking ownership back.
+    nrt->release(queuestruct->meminfo);
 
-    return orig_queue;
+    return queue_obj;
+}
+
+/*----------------------------------------------------------------------------*/
+/*--------------------- Box-unbox helpers for dpctl.SyclEvent       ----------*/
+/*----------------------------------------------------------------------------*/
+
+/*!
+ * @brief Helper to unbox a Python dpctl.SyclEvent object to a Numba-native
+ * eventstruct_t instance.
+ *
+ * @param    obj            A dpctl.SyclEvent Python object
+ * @param    event_struct   An instance of the struct numba-dpex uses to
+ *                          represent a dpctl.SyclEvent inside Numba.
+ * @return   {return}       Return code indicating success (0) or failure (-1).
+ */
+static int DPEXRT_sycl_event_from_python(NRT_api_functions *nrt,
+                                         PyObject *obj,
+                                         eventstruct_t *event_struct)
+{
+    struct PySyclEventObject *event_obj = NULL;
+    DPCTLSyclEventRef event_ref = NULL;
+
+    // We are unconditionally casting obj to a struct PySyclEventObject*. If
+    // the obj is not a struct PySyclEventObject* then the SyclEvent_GetEventRef
+    // will error out.
+    event_obj = (struct PySyclEventObject *)obj;
+
+    DPEXRT_DEBUG(
+        drt_debug_print("DPEXRT-DEBUG: In DPEXRT_sycl_event_from_python.\n"););
+
+    if (!(event_ref = SyclEvent_GetEventRef(event_obj))) {
+        DPEXRT_DEBUG(drt_debug_print(
+            "DPEXRT-ERROR: SyclEvent_GetEventRef returned NULL at "
+            "%s, line %d.\n",
+            __FILE__, __LINE__));
+        goto error;
+    }
+
+    // We are doing incref here to ensure python does not release the object
+    // while NRT references it. Coresponding decref is called by NRT in
+    // NRT_MemInfo_pyobject_dtor once there is no reference to this object by
+    // the code managed by NRT.
+    Py_INCREF(event_obj);
+    event_struct->meminfo =
+        nrt->manage_memory(event_obj, NRT_MemInfo_pyobject_dtor);
+    event_struct->parent = (PyObject *)event_obj;
+    event_struct->event_ref = event_ref;
+
+    return 0;
+
+error:
+    // If the check failed then decrement the refcount and return an error
+    // code of -1.
+    DPEXRT_DEBUG(drt_debug_print(
+        "DPEXRT-ERROR: Failed to unbox dpctl SyclEvent into a Numba "
+        "eventstruct at %s, line %d\n",
+        __FILE__, __LINE__));
+
+    return -1;
+}
+
+/*!
+ * @brief A helper function that boxes a Numba-dpex eventstruct_t object into a
+ * dctl.SyclEvent PyObject using the eventstruct_t's parent attribute.
+ *
+ * If there is no parent pointer stored in the eventstruct, then an error will
+ * be raised.
+ *
+ * @param    eventstruct    A Numba-dpex eventstruct object.
+ * @return   {return}       A PyObject created from the eventstruct->parent, if
+ *                          the PyObject could not be created return NULL.
+ */
+static PyObject *DPEXRT_sycl_event_to_python(NRT_api_functions *nrt,
+                                             eventstruct_t *eventstruct)
+{
+    PyObject *event_obj = eventstruct->parent;
+
+    if (event_obj == NULL) {
+        DPEXRT_DEBUG(
+            drt_debug_print("DPEXRT-DEBUG: creating new event object.\n"););
+        // SyclEvent_Make creates copy of event_ref so we don't need to manage
+        // nrt lifetime from python object.
+        event_obj = (PyObject *)SyclEvent_Make(eventstruct->event_ref);
+    }
+    else {
+        // Unfortunately we can not optimize (nrt->release that triggers
+        // Py_DECREF() from the destructor) and Py_INCREF() because nrt may need
+        // the object even if we return it to python.
+        // We need to increase reference count because we are returning new
+        // reference to the same event.
+        Py_INCREF(event_obj);
+    }
+
+    // We need to release meminfo since we no longer need this reference in nrt.
+    nrt->release(eventstruct->meminfo);
+
+    return event_obj;
+}
+
+/*!
+ * @brief A helper function that initializes Numba-dpex eventstruct_t object
+ * for the DPCTLSyclEventRef allocated inside dpjit. Parent is set to NULL.
+ *
+ * @param    nrt            A Numba pointer to public api functions.
+ * @param    event          A dpctl event reference.
+ * @param    eventstruct    A Numba-dpex eventstruct object (datamodel).
+ * @return   {return}       Nothing.
+ */
+static int DPEXRT_sycl_event_init(NRT_api_functions *nrt,
+                                  DPCTLSyclEventRef event,
+                                  eventstruct_t *eventstruct)
+{
+    if (eventstruct == NULL) {
+        DPEXRT_DEBUG(drt_debug_print(
+            "DPEXRT-ERROR: Failed to initialize dpctl SyclEvent into a Numba "
+            "eventstruct at %s, line %d. eventstruct is NULL.\n",
+            __FILE__, __LINE__));
+
+        return -1;
+    }
+
+    DPEXRT_DEBUG(
+        drt_debug_print("DPEXRT-DEBUG: creating new dpctl event meminfo.\n"););
+    eventstruct->parent = NULL;
+    eventstruct->event_ref = (void *)event;
+    // manage_memory sets ref count to 1.
+    eventstruct->meminfo =
+        nrt->manage_memory(event, NRT_MemInfo_EventRef_Delete);
+
+    return 0;
 }
 
 /*----------------------------------------------------------------------------*/
@@ -1306,6 +1498,14 @@ static PyObject *build_c_helpers_dict(void)
     _declpointer("DPEXRT_sycl_queue_from_python",
                  &DPEXRT_sycl_queue_from_python);
     _declpointer("DPEXRT_sycl_queue_to_python", &DPEXRT_sycl_queue_to_python);
+    _declpointer("DPEXRT_sycl_event_from_python",
+                 &DPEXRT_sycl_event_from_python);
+    _declpointer("DPEXRT_sycl_event_to_python", &DPEXRT_sycl_event_to_python);
+    _declpointer("DPEXRT_sycl_event_init", &DPEXRT_sycl_event_init);
+    _declpointer("DPEXRT_nrt_acquire_meminfo_and_schedule_release",
+                 &DPEXRT_nrt_acquire_meminfo_and_schedule_release);
+    _declpointer("DPEXRT_build_or_get_kernel", &DPEXRT_build_or_get_kernel);
+    _declpointer("DPEXRT_kernel_cache_size", &DPEXRT_kernel_cache_size);
 
 #undef _declpointer
     return dct;
@@ -1357,7 +1557,12 @@ MOD_INIT(_dpexrt_python)
                        PyLong_FromVoidPtr(&DPEXRT_sycl_queue_from_python));
     PyModule_AddObject(m, "DPEXRT_sycl_queue_to_python",
                        PyLong_FromVoidPtr(&DPEXRT_sycl_queue_to_python));
-
+    PyModule_AddObject(m, "DPEXRT_sycl_event_from_python",
+                       PyLong_FromVoidPtr(&DPEXRT_sycl_event_from_python));
+    PyModule_AddObject(m, "DPEXRT_sycl_event_to_python",
+                       PyLong_FromVoidPtr(&DPEXRT_sycl_event_to_python));
+    PyModule_AddObject(m, "DPEXRT_sycl_event_init",
+                       PyLong_FromVoidPtr(&DPEXRT_sycl_event_init));
     PyModule_AddObject(m, "DPEXRTQueue_CreateFromFilterString",
                        PyLong_FromVoidPtr(&DPEXRTQueue_CreateFromFilterString));
     PyModule_AddObject(m, "DpexrtQueue_SubmitRange",
@@ -1368,6 +1573,14 @@ MOD_INIT(_dpexrt_python)
                        PyLong_FromVoidPtr(&DPEXRT_MemInfo_alloc));
     PyModule_AddObject(m, "DPEXRT_MemInfo_fill",
                        PyLong_FromVoidPtr(&DPEXRT_MemInfo_fill));
+    PyModule_AddObject(
+        m, "DPEXRT_nrt_acquire_meminfo_and_schedule_release",
+        PyLong_FromVoidPtr(&DPEXRT_nrt_acquire_meminfo_and_schedule_release));
+    PyModule_AddObject(m, "DPEXRT_build_or_get_kernel",
+                       PyLong_FromVoidPtr(&DPEXRT_build_or_get_kernel));
+    PyModule_AddObject(m, "DPEXRT_kernel_cache_size",
+                       PyLong_FromVoidPtr(&DPEXRT_kernel_cache_size));
+
     PyModule_AddObject(m, "c_helpers", build_c_helpers_dict());
     return MOD_SUCCESS_VAL(m);
 }
diff --git a/numba_dpex/core/runtime/_eventstruct.c b/numba_dpex/core/runtime/_eventstruct.c
new file mode 100644
index 0000000000..be01288d8b
--- /dev/null
+++ b/numba_dpex/core/runtime/_eventstruct.c
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+
+#include "_eventstruct.h"
+#include "_dbg_printer.h"
+
+/*!
+ * @brief A destructor that is called from NRT on object destruction. Deletes
+ * dpctl event reference.
+ *
+ * @param    data           A dpctl event reference.
+ * @return   {return}       Nothing.
+ */
+void NRT_MemInfo_EventRef_Delete(void *data)
+{
+    DPCTLSyclEventRef eref = data;
+
+    DPCTLEvent_Delete(eref);
+
+    DPEXRT_DEBUG(
+        drt_debug_print("DPEXRT-DEBUG: deleting dpctl event reference.\n"););
+}
diff --git a/numba_dpex/core/runtime/_eventstruct.h b/numba_dpex/core/runtime/_eventstruct.h
new file mode 100644
index 0000000000..4163ebc7e5
--- /dev/null
+++ b/numba_dpex/core/runtime/_eventstruct.h
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Defines the numba-dpex native representation for a dpctl.SyclEvent
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef _EVENTSTRUCT_H_
+#define _EVENTSTRUCT_H_
+
+#include "_nrt_helper.h"
+#include "dpctl_sycl_interface.h"
+#include "numba/core/runtime/nrt_external.h"
+#include <Python.h>
+
+typedef struct
+{
+    NRT_MemInfo *meminfo;
+    PyObject *parent;
+    void *event_ref;
+} eventstruct_t;
+
+void NRT_MemInfo_EventRef_Delete(void *data);
+
+#endif /* _EVENTSTRUCT_H_ */
diff --git a/numba_dpex/core/runtime/_meminfo_helper.h b/numba_dpex/core/runtime/_meminfo_helper.h
index b2f2651a6f..b1129e06f2 100644
--- a/numba_dpex/core/runtime/_meminfo_helper.h
+++ b/numba_dpex/core/runtime/_meminfo_helper.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/runtime/_nrt_helper.c b/numba_dpex/core/runtime/_nrt_helper.c
index 8dfcdb03c6..c05cccd57c 100644
--- a/numba_dpex/core/runtime/_nrt_helper.c
+++ b/numba_dpex/core/runtime/_nrt_helper.c
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -134,3 +134,15 @@ void NRT_MemInfo_destroy(NRT_MemInfo *mi)
         TheMSys.stats.mi_free++;
     }
 }
+
+void NRT_MemInfo_pyobject_dtor(void *data)
+{
+    PyGILState_STATE gstate;
+    PyObject *ownerobj = data;
+
+    gstate = PyGILState_Ensure(); /* ensure the GIL */
+    Py_DECREF(data);              /* release the python object */
+    PyGILState_Release(gstate);   /* release the GIL */
+
+    DPEXRT_DEBUG(drt_debug_print("DPEXRT-DEBUG: pyobject destructor\n"););
+}
diff --git a/numba_dpex/core/runtime/_nrt_helper.h b/numba_dpex/core/runtime/_nrt_helper.h
index d6abdea9cd..23eef89471 100644
--- a/numba_dpex/core/runtime/_nrt_helper.h
+++ b/numba_dpex/core/runtime/_nrt_helper.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -19,5 +19,6 @@ size_t NRT_MemInfo_refcount(NRT_MemInfo *mi);
 void NRT_Free(void *ptr);
 void NRT_dealloc(NRT_MemInfo *mi);
 void NRT_MemInfo_destroy(NRT_MemInfo *mi);
+void NRT_MemInfo_pyobject_dtor(void *data);
 
 #endif /* _NRT_HELPER_H_ */
diff --git a/numba_dpex/core/runtime/_nrt_python_helper.c b/numba_dpex/core/runtime/_nrt_python_helper.c
index 91121baa60..7361519100 100644
--- a/numba_dpex/core/runtime/_nrt_python_helper.c
+++ b/numba_dpex/core/runtime/_nrt_python_helper.c
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/runtime/_nrt_python_helper.h b/numba_dpex/core/runtime/_nrt_python_helper.h
index 7f995f6e63..679b59fda7 100644
--- a/numba_dpex/core/runtime/_nrt_python_helper.h
+++ b/numba_dpex/core/runtime/_nrt_python_helper.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/runtime/_queuestruct.h b/numba_dpex/core/runtime/_queuestruct.h
index ebb088d65a..25b9ad0005 100644
--- a/numba_dpex/core/runtime/_queuestruct.h
+++ b/numba_dpex/core/runtime/_queuestruct.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -11,10 +11,12 @@
 
 #pragma once
 
+#include "numba/core/runtime/nrt_external.h"
 #include <Python.h>
 
 typedef struct
 {
+    NRT_MemInfo *meminfo;
     PyObject *parent;
     void *queue_ref;
 } queuestruct_t;
diff --git a/numba_dpex/core/runtime/_usmarraystruct.h b/numba_dpex/core/runtime/_usmarraystruct.h
index 695d54e23e..475b444467 100644
--- a/numba_dpex/core/runtime/_usmarraystruct.h
+++ b/numba_dpex/core/runtime/_usmarraystruct.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/runtime/context.py b/numba_dpex/core/runtime/context.py
index 16f8d30acd..1efa58cde7 100644
--- a/numba_dpex/core/runtime/context.py
+++ b/numba_dpex/core/runtime/context.py
@@ -1,9 +1,10 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 import functools
 
+import numba.core.unsafe.nrt
 from llvmlite import ir as llvmir
 from numba.core import cgutils, types
 
@@ -180,26 +181,82 @@ def arraystruct_from_python(self, pyapi, obj, ptr):
     def queuestruct_from_python(self, pyapi, obj, ptr):
         """Calls the c function DPEXRT_sycl_queue_from_python"""
         fnty = llvmir.FunctionType(
-            llvmir.IntType(32), [pyapi.pyobj, pyapi.voidptr]
+            llvmir.IntType(32), [pyapi.voidptr, pyapi.pyobj, pyapi.voidptr]
         )
+        nrt_api = self._context.nrt.get_nrt_api(pyapi.builder)
 
         fn = pyapi._get_function(fnty, "DPEXRT_sycl_queue_from_python")
         fn.args[0].add_attribute("nocapture")
         fn.args[1].add_attribute("nocapture")
+        fn.args[2].add_attribute("nocapture")
 
-        self.error = pyapi.builder.call(fn, (obj, ptr))
+        self.error = pyapi.builder.call(fn, (nrt_api, obj, ptr))
         return self.error
 
     def queuestruct_to_python(self, pyapi, val):
         """Calls the c function DPEXRT_sycl_queue_to_python"""
 
-        fnty = llvmir.FunctionType(pyapi.pyobj, [pyapi.voidptr])
+        fnty = llvmir.FunctionType(pyapi.pyobj, [pyapi.voidptr, pyapi.voidptr])
+        nrt_api = self._context.nrt.get_nrt_api(pyapi.builder)
 
         fn = pyapi._get_function(fnty, "DPEXRT_sycl_queue_to_python")
         fn.args[0].add_attribute("nocapture")
+        fn.args[1].add_attribute("nocapture")
+
         qptr = cgutils.alloca_once_value(pyapi.builder, val)
         ptr = pyapi.builder.bitcast(qptr, pyapi.voidptr)
-        self.error = pyapi.builder.call(fn, [ptr])
+
+        self.error = pyapi.builder.call(fn, [nrt_api, ptr])
+
+        return self.error
+
+    def eventstruct_from_python(self, pyapi, obj, ptr):
+        """Calls the c function DPEXRT_sycl_event_from_python"""
+        fnty = llvmir.FunctionType(
+            llvmir.IntType(32), [pyapi.voidptr, pyapi.pyobj, pyapi.voidptr]
+        )
+        nrt_api = self._context.nrt.get_nrt_api(pyapi.builder)
+
+        fn = pyapi._get_function(fnty, "DPEXRT_sycl_event_from_python")
+        fn.args[0].add_attribute("nocapture")
+        fn.args[1].add_attribute("nocapture")
+        fn.args[2].add_attribute("nocapture")
+
+        self.error = pyapi.builder.call(fn, (nrt_api, obj, ptr))
+        return self.error
+
+    def eventstruct_to_python(self, pyapi, val):
+        """Calls the c function DPEXRT_sycl_event_to_python"""
+
+        fnty = llvmir.FunctionType(pyapi.pyobj, [pyapi.voidptr, pyapi.voidptr])
+        nrt_api = self._context.nrt.get_nrt_api(pyapi.builder)
+
+        fn = pyapi._get_function(fnty, "DPEXRT_sycl_event_to_python")
+        fn.args[0].add_attribute("nocapture")
+        fn.args[1].add_attribute("nocapture")
+
+        qptr = cgutils.alloca_once_value(pyapi.builder, val)
+        ptr = pyapi.builder.bitcast(qptr, pyapi.voidptr)
+
+        self.error = pyapi.builder.call(fn, [nrt_api, ptr])
+
+        return self.error
+
+    def eventstruct_init(self, pyapi, event, struct):
+        """Calls the c function DPEXRT_sycl_event_init"""
+
+        fnty = llvmir.FunctionType(
+            llvmir.IntType(32), [pyapi.voidptr, pyapi.voidptr, pyapi.voidptr]
+        )
+        nrt_api = self._context.nrt.get_nrt_api(pyapi.builder)
+
+        fn = pyapi._get_function(fnty, "DPEXRT_sycl_event_init")
+        fn.args[0].add_attribute("nocapture")
+        fn.args[1].add_attribute("nocapture")
+        fn.args[2].add_attribute("nocapture")
+
+        ptr = pyapi.builder.bitcast(struct, pyapi.voidptr)
+        self.error = pyapi.builder.call(fn, [nrt_api, event, ptr])
 
         return self.error
 
@@ -377,26 +434,93 @@ def submit_ndrange(
 
         return ret
 
-    def copy_queue(self, builder, queue_ref):
-        """Calls DPCTLQueue_Copy to create a copy of the DpctlSyclQueueRef
-        pointer passed in to the function.
-
-        Args:
-            builder: The llvmlite.IRBuilder used to generate the LLVM IR for the
-            call.
-            queue_ref: An LLVM value for a DpctlSyclQueueRef pointer that will
-            be passed to the DPCTLQueue_Copy function.
+    def acquire_meminfo_and_schedule_release(
+        self, builder: llvmir.IRBuilder, args
+    ):
+        """Inserts LLVM IR to call nrt_acquire_meminfo_and_schedule_release.
+
+        DPCTLSyclEventRef
+        DPEXRT_nrt_acquire_meminfo_and_schedule_release(
+            NRT_api_functions *nrt,
+            DPCTLSyclQueueRef QRef,
+            NRT_MemInfo **meminfo_array,
+            size_t meminfo_array_size,
+            DPCTLSyclEventRef *depERefs,
+            size_t nDepERefs,
+            int *status,
+        );
 
-        Returns: A DPCTLSyclQueueRef pointer.
         """
         mod = builder.module
-        fnty = llvmir.FunctionType(
+
+        func_ty = llvmir.FunctionType(
             cgutils.voidptr_t,
-            [cgutils.voidptr_t],
+            [
+                cgutils.voidptr_t,
+                cgutils.voidptr_t,
+                cgutils.voidptr_t.as_pointer(),
+                llvmir.IntType(64),
+                cgutils.voidptr_t.as_pointer(),
+                llvmir.IntType(64),
+                llvmir.IntType(64).as_pointer(),
+            ],
         )
-        fn = cgutils.get_or_insert_function(mod, fnty, "DPCTLQueue_Copy")
-        fn.return_value.add_attribute("noalias")
+        fn = cgutils.get_or_insert_function(
+            mod, func_ty, "DPEXRT_nrt_acquire_meminfo_and_schedule_release"
+        )
+        ret = builder.call(fn, args)
+
+        return ret
+
+    def build_or_get_kernel(self, builder: llvmir.IRBuilder, args):
+        """Inserts LLVM IR to call build_or_get_kernel.
 
-        ret = builder.call(fn, [queue_ref])
+        DPCTLSyclKernelRef
+        DPEXRT_build_or_get_kernel(
+            const DPCTLSyclContextRef ctx,
+            const DPCTLSyclDeviceRef dev,
+            size_t il_hash,
+            const char *il,
+            size_t il_length,
+            const char *compile_opts,
+            const char *kernel_name,
+        );
+
+        """
+        mod = builder.module
+
+        func_ty = llvmir.FunctionType(
+            cgutils.voidptr_t,
+            [
+                cgutils.voidptr_t,
+                cgutils.voidptr_t,
+                llvmir.IntType(64),
+                cgutils.voidptr_t,
+                llvmir.IntType(64),
+                cgutils.voidptr_t,
+                cgutils.voidptr_t,
+            ],
+        )
+        fn = cgutils.get_or_insert_function(
+            mod, func_ty, "DPEXRT_build_or_get_kernel"
+        )
+        ret = builder.call(fn, args)
 
         return ret
+
+    def kernel_cache_size(self, builder: llvmir.IRBuilder):
+        """Inserts LLVM IR to call kernel_cache_size.
+
+        size_t DPEXRT_kernel_cache_size();
+
+        """
+        fn = cgutils.get_or_insert_function(
+            builder.module,
+            llvmir.FunctionType(
+                llvmir.IntType(64),
+                [],
+            ),
+            "DPEXRT_kernel_cache_size",
+        )
+
+        return builder.call(fn, [])
diff --git a/numba_dpex/core/runtime/experimental/kernel_caching.cpp b/numba_dpex/core/runtime/experimental/kernel_caching.cpp
new file mode 100644
index 0000000000..56dfd94fcc
--- /dev/null
+++ b/numba_dpex/core/runtime/experimental/kernel_caching.cpp
@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+
+#include "kernel_caching.h"
+#include <unordered_map>
+
+extern "C"
+{
+#include "dpctl_capi.h"
+#include "dpctl_sycl_interface.h"
+
+#include "_dbg_printer.h"
+
+#include "numba/core/runtime/nrt_external.h"
+}
+
+#include "syclinterface/dpctl_sycl_type_casters.hpp"
+#include "tools/boost_hash.hpp"
+#include "tools/dpctl.hpp"
+
+using CacheKey = std::tuple<DPCTLSyclContextRef, DPCTLSyclDeviceRef, size_t>;
+
+namespace std
+{
+template <> struct hash<CacheKey>
+{
+    size_t operator()(const CacheKey &ck) const
+    {
+        std::size_t seed = 0;
+        boost::hash_combine(seed, std::get<DPCTLSyclDeviceRef>(ck));
+        boost::hash_combine(seed, std::get<DPCTLSyclContextRef>(ck));
+        boost::hash_detail::hash_combine_impl(seed, std::get<size_t>(ck));
+        return seed;
+    }
+};
+template <> struct equal_to<CacheKey>
+{
+    constexpr bool operator()(const CacheKey &lhs, const CacheKey &rhs) const
+    {
+        return DPCTLDevice_AreEq(std::get<DPCTLSyclDeviceRef>(lhs),
+                                 std::get<DPCTLSyclDeviceRef>(rhs)) &&
+               DPCTLContext_AreEq(std::get<DPCTLSyclContextRef>(lhs),
+                                  std::get<DPCTLSyclContextRef>(rhs)) &&
+               std::get<size_t>(lhs) == std::get<size_t>(rhs);
+    }
+};
+} // namespace std
+
+// TODO: add cache cleaning
+// https://github.com/IntelPython/numba-dpex/issues/1240
+std::unordered_map<CacheKey, DPCTLSyclKernelRef> sycl_kernel_cache =
+    std::unordered_map<CacheKey, DPCTLSyclKernelRef>();
+
+template <class M, class Key, class F>
+typename M::mapped_type &get_else_compute(M &m, Key const &k, F f)
+{
+    typedef typename M::mapped_type V;
+    std::pair<typename M::iterator, bool> r =
+        m.insert(typename M::value_type(k, V()));
+    V &v = r.first->second;
+    if (r.second) {
+        DPEXRT_DEBUG(drt_debug_print("DPEXRT-DEBUG: building kernel.\n"););
+        f(v);
+    }
+    else {
+        DPEXRT_DEBUG(drt_debug_print("DPEXRT-DEBUG: using cached kernel.\n"););
+        DPCTLDevice_Delete(std::get<DPCTLSyclDeviceRef>(k));
+        DPCTLContext_Delete(std::get<DPCTLSyclContextRef>(k));
+    }
+    return v;
+}
+
+extern "C"
+{
+    DPCTLSyclKernelRef DPEXRT_build_or_get_kernel(const DPCTLSyclContextRef ctx,
+                                                  const DPCTLSyclDeviceRef dev,
+                                                  size_t il_hash,
+                                                  const char *il,
+                                                  size_t il_length,
+                                                  const char *compile_opts,
+                                                  const char *kernel_name)
+    {
+        DPEXRT_DEBUG(
+            drt_debug_print("DPEXRT-DEBUG: in build or get kernel.\n"););
+
+        CacheKey key = std::make_tuple(ctx, dev, il_hash);
+
+        DPEXRT_DEBUG(auto ctx_hash = std::hash<DPCTLSyclContextRef>{}(ctx);
+                     auto dev_hash = std::hash<DPCTLSyclDeviceRef>{}(dev);
+                     drt_debug_print("DPEXRT-DEBUG: key hashes: %d %d %d.\n",
+                                     ctx_hash, dev_hash, il_hash););
+
+        auto k_ref = get_else_compute(
+            sycl_kernel_cache, key,
+            [ctx, dev, il, il_length, compile_opts,
+             kernel_name](DPCTLSyclKernelRef &k_ref) {
+                auto kb_ref = DPCTLKernelBundle_CreateFromSpirv(
+                    ctx, dev, il, il_length, compile_opts);
+                k_ref = DPCTLKernelBundle_GetKernel(kb_ref, kernel_name);
+                DPCTLKernelBundle_Delete(kb_ref);
+            });
+
+        DPEXRT_DEBUG(drt_debug_print("DPEXRT-DEBUG: kernel hash size: %d.\n",
+                                     sycl_kernel_cache.size()););
+
+        return DPCTLKernel_Copy(k_ref);
+    }
+
+    size_t DPEXRT_kernel_cache_size() { return sycl_kernel_cache.size(); }
+}
diff --git a/numba_dpex/core/runtime/experimental/kernel_caching.h b/numba_dpex/core/runtime/experimental/kernel_caching.h
new file mode 100644
index 0000000000..042f7d61db
--- /dev/null
+++ b/numba_dpex/core/runtime/experimental/kernel_caching.h
@@ -0,0 +1,54 @@
+// SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Defines dpex run time function(s) that cache kernel on device.
+///
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "dpctl_capi.h"
+#include "dpctl_sycl_interface.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+    /*!
+     * @brief returns dpctl kernel reference for the SPIRV file on particular
+     * device. Compiles only first time, all others will use cache for the same
+     * input. It steals reference to context and device because we need to keep
+     * it alive for cache keys.
+     *
+     * @param    ctx            Context reference,
+     * @param    dev            Device reference,
+     * @param    il_hash        Hash of the SPIRV binary data,
+     * @param    il             SPIRV binary data,
+     * @param    il_length      SPIRV binary data size,
+     * @param    compile_opts   compile options,
+     * @param    kernel_name    kernel name inside SPIRV binary data to return
+     * reference to.
+     *
+     * @return   {return}       Kernel reference to the compiled SPIR-V.
+     */
+    DPCTLSyclKernelRef DPEXRT_build_or_get_kernel(const DPCTLSyclContextRef ctx,
+                                                  const DPCTLSyclDeviceRef dev,
+                                                  size_t il_hash,
+                                                  const char *il,
+                                                  size_t il_length,
+                                                  const char *compile_opts,
+                                                  const char *kernel_name);
+
+    /*!
+     * @brief returns cache size. Intended for test purposes only
+     *
+     * @return   {return}       Kernel cache size.
+     */
+    size_t DPEXRT_kernel_cache_size();
+#ifdef __cplusplus
+}
+#endif
diff --git a/numba_dpex/core/runtime/experimental/nrt_reserve_meminfo.cpp b/numba_dpex/core/runtime/experimental/nrt_reserve_meminfo.cpp
new file mode 100644
index 0000000000..1d03880789
--- /dev/null
+++ b/numba_dpex/core/runtime/experimental/nrt_reserve_meminfo.cpp
@@ -0,0 +1,71 @@
+// SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+
+#include "nrt_reserve_meminfo.h"
+
+#include "_dbg_printer.h"
+#include "syclinterface/dpctl_sycl_type_casters.hpp"
+#include <CL/sycl.hpp>
+
+extern "C"
+{
+    DPCTLSyclEventRef
+    DPEXRT_nrt_acquire_meminfo_and_schedule_release(NRT_api_functions *nrt,
+                                                    DPCTLSyclQueueRef QRef,
+                                                    NRT_MemInfo **meminfo_array,
+                                                    size_t meminfo_array_size,
+                                                    DPCTLSyclEventRef *depERefs,
+                                                    size_t nDepERefs,
+                                                    int *status)
+    {
+        DPEXRT_DEBUG(drt_debug_print(
+                         "DPEXRT-DEBUG: scheduling nrt meminfo release.\n"););
+
+        using dpctl::syclinterface::unwrap;
+        using dpctl::syclinterface::wrap;
+
+        sycl::queue *q = unwrap<sycl::queue>(QRef);
+
+        std::vector<NRT_MemInfo *> meminfo_vec(
+            meminfo_array, meminfo_array + meminfo_array_size);
+
+        for (size_t i = 0; i < meminfo_array_size; ++i) {
+            nrt->acquire(meminfo_vec[i]);
+        }
+
+        DPEXRT_DEBUG(drt_debug_print("DPEXRT-DEBUG: acquired meminfo.\n"););
+
+        try {
+            sycl::event ht_ev = q->submit([&](sycl::handler &cgh) {
+                for (size_t ev_id = 0; ev_id < nDepERefs; ++ev_id) {
+                    cgh.depends_on(*(unwrap<sycl::event>(depERefs[ev_id])));
+                }
+                cgh.host_task([meminfo_array_size, meminfo_vec, nrt]() {
+                    for (size_t i = 0; i < meminfo_array_size; ++i) {
+                        nrt->release(meminfo_vec[i]);
+                        DPEXRT_DEBUG(
+                            drt_debug_print("DPEXRT-DEBUG: released meminfo "
+                                            "from host_task.\n"););
+                    }
+                });
+            });
+
+            constexpr int result_ok = 0;
+
+            *status = result_ok;
+            auto e_ptr = new sycl::event(ht_ev);
+            return wrap<sycl::event>(e_ptr);
+        } catch (const std::exception &e) {
+            constexpr int result_std_exception = 1;
+
+            *status = result_std_exception;
+            return nullptr;
+        }
+
+        constexpr int result_other_abnormal = 2;
+
+        *status = result_other_abnormal;
+        return nullptr;
+    }
+}
diff --git a/numba_dpex/core/runtime/experimental/nrt_reserve_meminfo.h b/numba_dpex/core/runtime/experimental/nrt_reserve_meminfo.h
new file mode 100644
index 0000000000..8ad9576f3b
--- /dev/null
+++ b/numba_dpex/core/runtime/experimental/nrt_reserve_meminfo.h
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Defines dpctl style function(s) that interact with nrt meminfo and sycl.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef _EXPERIMENTAL_H_
+#define _EXPERIMENTAL_H_
+
+#include "dpctl_capi.h"
+#include "numba/core/runtime/nrt_external.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+    /*!
+     * @brief Acquires meminfos and schedules a host task to release them.
+     *
+     * @param    nrt            NRT public API functions,
+     * @param    QRef           Queue reference,
+     * @param    meminfo_array  Array of meminfo pointers to perform actions on,
+     * @param    meminfo_array_size Length of meminfo_array,
+     * @param    depERefs       Array of dependent events for the host task,
+     * @param    nDepERefs      Length of depERefs,
+     * @param    status         Variable to write status to. Same style as
+     * dpctl,
+     * @return   {return}       Event reference to the host task.
+     */
+    DPCTLSyclEventRef
+    DPEXRT_nrt_acquire_meminfo_and_schedule_release(NRT_api_functions *nrt,
+                                                    DPCTLSyclQueueRef QRef,
+                                                    NRT_MemInfo **meminfo_array,
+                                                    size_t meminfo_array_size,
+                                                    DPCTLSyclEventRef *depERefs,
+                                                    size_t nDepERefs,
+                                                    int *status);
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _EXPERIMENTAL_H_ */
diff --git a/numba_dpex/core/runtime/experimental/tools/boost_hash.hpp b/numba_dpex/core/runtime/experimental/tools/boost_hash.hpp
new file mode 100644
index 0000000000..7d96bdc31f
--- /dev/null
+++ b/numba_dpex/core/runtime/experimental/tools/boost_hash.hpp
@@ -0,0 +1,38 @@
+// Copyright 2005-2014 Daniel James.
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+//  Based on Peter Dimov's proposal
+//  http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2005/n1756.pdf
+//  issue 6.18.
+//
+//  This also contains public domain code from MurmurHash. From the
+//  MurmurHash header:
+
+// MurmurHash3 was written by Austin Appleby, and is placed in the public
+// domain. The author hereby disclaims copyright to this source code.
+
+// 2023 Intel Corporation
+// Copied hash_combine and hash_combine_impl from boost
+// (https://www.boost.org/doc/libs/1_76_0/boost/container_hash/hash.hpp) and
+// changed hash_combine to use std::hash<T> instead of boost::hash<T>.
+
+#include <functional>
+
+namespace boost
+{
+namespace hash_detail
+{
+template <typename SizeT>
+inline void hash_combine_impl(SizeT &seed, SizeT value)
+{
+    seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+}
+} // namespace hash_detail
+
+template <class T> inline void hash_combine(std::size_t &seed, T const &v)
+{
+    std::hash<T> hasher;
+    return boost::hash_detail::hash_combine_impl(seed, hasher(v));
+}
+} // namespace boost
diff --git a/numba_dpex/core/runtime/experimental/tools/dpctl.cpp b/numba_dpex/core/runtime/experimental/tools/dpctl.cpp
new file mode 100644
index 0000000000..c9187c4f70
--- /dev/null
+++ b/numba_dpex/core/runtime/experimental/tools/dpctl.cpp
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+
+#include "dpctl.hpp"
+#include <CL/sycl.hpp>
+
+namespace std
+{
+
+size_t
+hash<DPCTLSyclDeviceRef>::operator()(const DPCTLSyclDeviceRef &DRef) const
+{
+    using dpctl::syclinterface::unwrap;
+    return hash<sycl::device>()(*unwrap<sycl::device>(DRef));
+}
+
+size_t
+hash<DPCTLSyclContextRef>::operator()(const DPCTLSyclContextRef &CRef) const
+{
+    using dpctl::syclinterface::unwrap;
+    return hash<sycl::context>()(*unwrap<sycl::context>(CRef));
+}
+} // namespace std
diff --git a/numba_dpex/core/runtime/experimental/tools/dpctl.hpp b/numba_dpex/core/runtime/experimental/tools/dpctl.hpp
new file mode 100644
index 0000000000..9fa03b9acb
--- /dev/null
+++ b/numba_dpex/core/runtime/experimental/tools/dpctl.hpp
@@ -0,0 +1,26 @@
+// SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Defines overloads to dpctl library that eventually must be ported there.
+///
+//===----------------------------------------------------------------------===//
+
+#pragma once
+#include "syclinterface/dpctl_sycl_type_casters.hpp"
+
+namespace std
+{
+template <> struct hash<DPCTLSyclDeviceRef>
+{
+    size_t operator()(const DPCTLSyclDeviceRef &DRef) const;
+};
+
+template <> struct hash<DPCTLSyclContextRef>
+{
+    size_t operator()(const DPCTLSyclContextRef &CRef) const;
+};
+} // namespace std
diff --git a/numba_dpex/core/targets/__init__.py b/numba_dpex/core/targets/__init__.py
index 00fec5f515..7206ff64a6 100644
--- a/numba_dpex/core/targets/__init__.py
+++ b/numba_dpex/core/targets/__init__.py
@@ -1,3 +1,3 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/core/targets/dpjit_target.py b/numba_dpex/core/targets/dpjit_target.py
index 2d3d42edd0..cb464bc5c7 100644
--- a/numba_dpex/core/targets/dpjit_target.py
+++ b/numba_dpex/core/targets/dpjit_target.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -7,13 +7,14 @@
 
 from functools import cached_property
 
-from numba.core import utils
-from numba.core.codegen import JITCPUCodegen
+from numba.core import typing
 from numba.core.compiler_lock import global_compiler_lock
 from numba.core.cpu import CPUContext
-from numba.core.imputils import Registry, RegistryLoader
+from numba.core.imputils import Registry
 from numba.core.target_extension import CPU, target_registry
 
+from numba_dpex.dpnp_iface import dpnp_ufunc_db
+
 
 class Dpex(CPU):
     pass
@@ -25,22 +26,34 @@ class Dpex(CPU):
 # permits lookup and reference in user space by the string "dpex"
 target_registry[DPEX_TARGET_NAME] = Dpex
 
-# This is the function registry for the dpu, it just has one registry, this one!
 dpex_function_registry = Registry()
 
 
+class DpexTypingContext(typing.Context):
+    """Custom typing context to support dpjit compilation."""
+
+    def load_additional_registries(self):
+        """Register dpjit specific functions like dpnp ufuncs."""
+        from numba_dpex.core.typing import dpnpdecl
+
+        self.install_registry(dpnpdecl.registry)
+        super().load_additional_registries()
+
+
 class DpexTargetContext(CPUContext):
     def __init__(self, typingctx, target=DPEX_TARGET_NAME):
         super().__init__(typingctx, target)
 
     @global_compiler_lock
     def init(self):
-        self.is32bit = utils.MACHINE_BITS == 32
-        self._internal_codegen = JITCPUCodegen("numba.exec")
         self.lower_extensions = {}
+        super().init()
+
+        # TODO: initialize nrt once switched to nrt from drt. Most likely we
+        # call it somewhere. Double check.
+        # https://github.com/IntelPython/numba-dpex/issues/1175
         # Initialize NRT runtime
-        # rtsys.initialize(self)
-        self.refresh()
+        # rtsys.initialize(self) # noqa: E800
 
     @cached_property
     def dpexrt(self):
@@ -48,15 +61,15 @@ def dpexrt(self):
 
         return DpexRTContext(self)
 
-    def refresh(self):
-        registry = dpex_function_registry
-        try:
-            loader = self._registries[registry]
-        except KeyError:
-            loader = RegistryLoader(registry)
-            self._registries[registry] = loader
-        self.install_registry(registry)
-        # Also refresh typing context, since @overload declarations can
-        # affect it.
-        self.typing_context.refresh()
-        super().refresh()
+    def load_additional_registries(self):
+        """
+        Load dpjit-specific registries.
+        """
+        self.install_registry(dpex_function_registry)
+
+        # loading CPU specific registries
+        super().load_additional_registries()
+
+    # TODO: do we need it?
+    def get_ufunc_info(self, ufunc_key):
+        return dpnp_ufunc_db.get_ufunc_info(ufunc_key)
diff --git a/numba_dpex/core/targets/kernel_target.py b/numba_dpex/core/targets/kernel_target.py
index 699ac17e68..33d32fc9e1 100644
--- a/numba_dpex/core/targets/kernel_target.py
+++ b/numba_dpex/core/targets/kernel_target.py
@@ -1,8 +1,8 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-import re
+from enum import IntEnum
 from functools import cached_property
 
 import dpnp
@@ -19,7 +19,7 @@
 from numba_dpex.core.datamodel.models import _init_data_model_manager
 from numba_dpex.core.exceptions import UnsupportedKernelArgumentError
 from numba_dpex.core.typeconv import to_usm_ndarray
-from numba_dpex.core.types import DpnpNdArray, USMNdArray
+from numba_dpex.core.types import USMNdArray
 from numba_dpex.core.utils import get_info_from_suai
 from numba_dpex.utils import address_space, calling_conv
 
@@ -27,11 +27,31 @@
 
 CC_SPIR_KERNEL = "spir_kernel"
 CC_SPIR_FUNC = "spir_func"
-VALID_CHARS = re.compile(r"[^a-z0-9]", re.I)
-LINK_ATOMIC = 111
 LLVM_SPIRV_ARGS = 112
 
 
+class CompilationMode(IntEnum):
+    """Flags used to determine how a function should be compiled by the
+    numba_dpex.experimental.dispatcher.KernelDispatcher. Note the functionality
+    will be merged into numba_dpex.core.kernel_interface.dispatcher in the
+    future.
+
+        KERNEL :         Indicates that the function will be compiled into an
+                         LLVM function that has ``spir_kernel`` calling
+                         convention and is compiled down to SPIR-V.
+                         Additionally, the function cannot return any value and
+                         input arguments to the function have to adhere to
+                         "compute follows data" to ensure execution queue
+                         inference.
+        DEVICE_FUNCTION: Indicates that the function will be compiled into an
+                         LLVM function that has ``spir_func`` calling convention
+                         and will be compiled only into LLVM bitcode.
+    """
+
+    KERNEL = 1
+    DEVICE_FUNC = 2
+
+
 class DpexKernelTypingContext(typing.BaseContext):
     """Custom typing context to support kernel compilation.
 
@@ -89,14 +109,19 @@ def resolve_argument_type(self, val):
 
     def load_additional_registries(self):
         """Register the OpenCL API and math and other functions."""
-        from numba.core.typing import cmathdecl, npydecl
+        from numba.core.typing import cmathdecl, enumdecl, npydecl
+
+        from numba_dpex.core.typing import dpnpdecl
 
         from ...ocl import mathdecl, ocldecl
 
         self.install_registry(ocldecl.registry)
         self.install_registry(mathdecl.registry)
         self.install_registry(cmathdecl.registry)
+        # TODO: https://github.com/IntelPython/numba-dpex/issues/1270
         self.install_registry(npydecl.registry)
+        self.install_registry(dpnpdecl.registry)
+        self.install_registry(enumdecl.registry)
 
 
 class SyclDevice(GPU):
@@ -105,7 +130,7 @@ class SyclDevice(GPU):
     pass
 
 
-DPEX_KERNEL_TARGET_NAME = "SyclDevice"
+DPEX_KERNEL_TARGET_NAME = "dpex_kernel"
 
 target_registry[DPEX_KERNEL_TARGET_NAME] = SyclDevice
 
@@ -165,7 +190,7 @@ def _gen_arg_base_type(self, fn):
         name = llvmir.MetaDataString(mod, "kernel_arg_base_type")
         return mod.add_metadata([name] + consts)
 
-    def _finalize_wrapper_module(self, fn):
+    def _finalize_kernel_wrapper_module(self, fn):
         """Add metadata and calling convention to the wrapper function.
 
         The helper function adds function metadata to the wrapper function and
@@ -177,41 +202,12 @@ def _finalize_wrapper_module(self, fn):
             fn: LLVM function representing the "kernel" wrapper function.
 
         """
-        mod = fn.module
         # Set norecurse
         fn.attributes.add("norecurse")
         # Set SPIR kernel calling convention
         fn.calling_convention = CC_SPIR_KERNEL
 
-        # Mark kernels
-        ocl_kernels = cgutils.get_or_insert_named_metadata(
-            mod, "opencl.kernels"
-        )
-        ocl_kernels.add(
-            mod.add_metadata(
-                [
-                    fn,
-                    self._gen_arg_addrspace_md(fn),
-                    self._gen_arg_type(fn),
-                    self._gen_arg_type_qual(fn),
-                    self._gen_arg_base_type(fn),
-                ],
-            )
-        )
-
-        # Other metadata
-        others = [
-            "opencl.used.extensions",
-            "opencl.used.optional.core.features",
-            "opencl.compiler.options",
-        ]
-
-        for name in others:
-            nmd = cgutils.get_or_insert_named_metadata(mod, name)
-            if not nmd.operands:
-                mod.add_metadata([])
-
-    def _generate_kernel_wrapper(self, func, argtypes):
+    def _generate_spir_kernel_wrapper(self, func, argtypes):
         module = func.module
         arginfo = self.get_arg_packer(argtypes)
         wrapperfnty = llvmir.FunctionType(
@@ -227,7 +223,7 @@ def _generate_kernel_wrapper(self, func, argtypes):
         func = llvmir.Function(wrapper_module, fnty, name=func.name)
         func.calling_convention = CC_SPIR_FUNC
         wrapper = llvmir.Function(wrapper_module, wrapperfnty, name=wrappername)
-        builder = llvmir.IRBuilder(wrapper.append_basic_block(""))
+        builder = llvmir.IRBuilder(wrapper.append_basic_block("entry"))
 
         callargs = arginfo.from_arguments(builder, wrapper.args)
 
@@ -237,7 +233,7 @@ def _generate_kernel_wrapper(self, func, argtypes):
         )
         builder.ret_void()
 
-        self._finalize_wrapper_module(wrapper)
+        self._finalize_kernel_wrapper_module(wrapper)
 
         # Link the spir_func module to the wrapper module
         module.link_in(ll.parse_assembly(str(wrapper_module)))
@@ -251,27 +247,28 @@ def __init__(self, typingctx, target=DPEX_KERNEL_TARGET_NAME):
         super().__init__(typingctx, target)
 
     def init(self):
-        self._internal_codegen = codegen.JITSPIRVCodegen("numba_dpex.jit")
+        """Called by the super().__init__ constructor to initalize the child
+        class.
+        """
+        self._internal_codegen = codegen.JITSPIRVCodegen("numba_dpex.kernel")
         self._target_data = ll.create_target_data(
             codegen.SPIR_DATA_LAYOUT[utils.MACHINE_BITS]
         )
-        # Override data model manager to SPIR model
-        import numba.cpython.unicode
 
+        # Override data model manager to SPIR model
         self.data_model_manager = _init_data_model_manager()
         self.extra_compile_options = dict()
 
-        import copy
+        from numba_dpex.dpnp_iface.dpnp_ufunc_db import _lazy_init_dpnp_db
 
-        from numba.np.ufunc_db import _lazy_init_db
+        _lazy_init_dpnp_db()
 
-        _lazy_init_db()
-        from numba.np.ufunc_db import _ufunc_db as ufunc_db
+        # we need to import it after, because before init it is None and
+        # variable is passed by value
+        from numba_dpex.dpnp_iface.dpnp_ufunc_db import _dpnp_ufunc_db
 
-        self.ufunc_db = copy.deepcopy(ufunc_db)
-        self.cpu_context = cpu_target.target_context
+        self.ufunc_db = _dpnp_ufunc_db
 
-    # Overrides
     def create_module(self, name):
         return self._internal_codegen._create_empty_module(name)
 
@@ -355,14 +352,13 @@ def mangler(self, name, argtypes, abi_tags=(), uid=None):
             name + "dpex_fn", argtypes, abi_tags=abi_tags, uid=uid
         )
 
-    def prepare_ocl_kernel(self, func, argtypes):
-        module = func.module
+    def prepare_spir_kernel(self, func, argtypes):
         func.linkage = "linkonce_odr"
-        module.data_layout = codegen.SPIR_DATA_LAYOUT[self.address_size]
-        wrapper = self._generate_kernel_wrapper(func, argtypes)
+        func.module.data_layout = codegen.SPIR_DATA_LAYOUT[self.address_size]
+        wrapper = self._generate_spir_kernel_wrapper(func, argtypes)
         return wrapper
 
-    def mark_ocl_device(self, func):
+    def set_spir_func_calling_conv(self, func):
         # Adapt to SPIR
         func.calling_convention = CC_SPIR_FUNC
         func.linkage = "linkonce_odr"
@@ -436,17 +432,24 @@ def addrspacecast(self, builder, src, addrspace):
         ptras = llvmir.PointerType(src.type.pointee, addrspace=addrspace)
         return builder.addrspacecast(src, ptras)
 
-    # Overrides
     def get_ufunc_info(self, ufunc_key):
         return self.ufunc_db[ufunc_key]
 
+    def populate_array(self, arr, **kwargs):
+        """
+        Populate array structure.
+        """
+        from numba_dpex.core.kernel_interface import arrayobj
+
+        return arrayobj.populate_array(arr, **kwargs)
+
 
 class DpexCallConv(MinimalCallConv):
     """Custom calling convention class used by numba-dpex.
 
     numba_dpex's calling convention derives from
     :class:`numba.core.callconv import MinimalCallConv`. The
-    :class:`DpexCallConv` overriddes :func:`call_function`.
+    :class:`DpexCallConv` overrides :func:`call_function`.
 
     """
 
diff --git a/numba_dpex/core/typeconv/__init__.py b/numba_dpex/core/typeconv/__init__.py
index 61bb97a839..55d09d746a 100644
--- a/numba_dpex/core/typeconv/__init__.py
+++ b/numba_dpex/core/typeconv/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/typeconv/array_conversion.py b/numba_dpex/core/typeconv/array_conversion.py
index 5b7472c59b..d56ea9b6fe 100644
--- a/numba_dpex/core/typeconv/array_conversion.py
+++ b/numba_dpex/core/typeconv/array_conversion.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/types/__init__.py b/numba_dpex/core/types/__init__.py
index df0444115b..398476797b 100644
--- a/numba_dpex/core/types/__init__.py
+++ b/numba_dpex/core/types/__init__.py
@@ -1,9 +1,9 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 from .array_type import Array
-from .dpctl_types import DpctlSyclQueue
+from .dpctl_types import DpctlSyclEvent, DpctlSyclQueue
 from .dpnp_ndarray_type import DpnpNdArray
 from .numba_types_short_names import (
     b1,
@@ -26,6 +26,7 @@
     uint64,
     void,
 )
+from .range_types import NdRangeType, RangeType
 from .usm_ndarray_type import USMNdArray
 
 usm_ndarray = USMNdArray
@@ -33,7 +34,10 @@
 __all__ = [
     "Array",
     "DpctlSyclQueue",
+    "DpctlSyclEvent",
     "DpnpNdArray",
+    "RangeType",
+    "NdRangeType",
     "USMNdArray",
     "none",
     "boolean",
diff --git a/numba_dpex/core/types/array_type.py b/numba_dpex/core/types/array_type.py
index 76cda9a535..737d08db84 100644
--- a/numba_dpex/core/types/array_type.py
+++ b/numba_dpex/core/types/array_type.py
@@ -1,10 +1,8 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 import numpy as np
-from numba.core import types
-from numba.core.datamodel.models import StructModel
 from numba.core.types.npytypes import Array
 
 
diff --git a/numba_dpex/core/types/dpctl_types.py b/numba_dpex/core/types/dpctl_types.py
index f7d03ba04a..969fd9b402 100644
--- a/numba_dpex/core/types/dpctl_types.py
+++ b/numba_dpex/core/types/dpctl_types.py
@@ -1,10 +1,10 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 import random
 
-from dpctl import SyclQueue
+from dpctl import SyclEvent, SyclQueue
 from numba import types
 from numba.core import cgutils
 from numba.extending import NativeValue, box, unbox
@@ -23,12 +23,16 @@ def __init__(self, sycl_queue):
         # XXX: Storing the device filter string is a temporary workaround till
         # the compute follows data inference pass is fixed to use SyclQueue
         self._device = sycl_queue.sycl_device.filter_string
-
+        self._device_has_aspect_atomic64 = (
+            sycl_queue.sycl_device.has_aspect_atomic64
+        )
         try:
             self._unique_id = hash(sycl_queue)
         except Exception:
             self._unique_id = self.rand_digit_str(16)
-        super(DpctlSyclQueue, self).__init__(name="DpctlSyclQueue")
+        super(DpctlSyclQueue, self).__init__(
+            name=f"DpctlSyclQueue on {self._device}"
+        )
 
     def rand_digit_str(self, n):
         return "".join(
@@ -45,14 +49,20 @@ def sycl_device(self):
         """
         return self._device
 
+    @property
+    def device_has_aspect_atomic64(self):
+        return self._device_has_aspect_atomic64
+
     @property
     def key(self):
         """Returns a Python object used as the key to cache an instance of
         DpctlSyclQueue.
+
         The key is constructed by hashing the actual dpctl.SyclQueue object
         encapsulated by an instance of DpctlSyclQueue. Doing so ensures, that
         different dpctl.SyclQueue instances are inferred as separate instances
         of the DpctlSyclQueue type.
+
         Returns:
             int: hash of the self._sycl_queue Python object.
         """
@@ -118,3 +128,71 @@ def box_sycl_queue(typ, val, c):
         return queue
     else:
         raise UnreachableError
+
+
+class DpctlSyclEvent(types.Type):
+    """A Numba type to represent a dpctl.SyclEvent PyObject."""
+
+    def __init__(self):
+        super(DpctlSyclEvent, self).__init__(name="DpctlSyclEvent")
+
+    @property
+    def box_type(self):
+        return SyclEvent
+
+
+@unbox(DpctlSyclEvent)
+def unbox_sycl_event(typ, obj, c):
+    """
+    Convert a SyclEvent object to a native structure.
+    """
+
+    qstruct = cgutils.create_struct_proxy(typ)(c.context, c.builder)
+    qptr = qstruct._getpointer()
+    ptr = c.builder.bitcast(qptr, c.pyapi.voidptr)
+
+    dpexrtCtx = dpexrt.DpexRTContext(c.context)
+    errcode = dpexrtCtx.eventstruct_from_python(c.pyapi, obj, ptr)
+    is_error = cgutils.is_not_null(c.builder, errcode)
+
+    # Handle error
+    with c.builder.if_then(is_error, likely=False):
+        c.pyapi.err_set_string(
+            "PyExc_TypeError",
+            "can't unbox dpctl.SyclEvent from PyObject into a Numba "
+            "native value. The object maybe of a different type",
+        )
+
+    return NativeValue(c.builder.load(qptr), is_error=is_error)
+
+
+@box(DpctlSyclEvent)
+def box_sycl_event(typ, val, c):
+    """Boxes a NativeValue representation of DpctlSyclEvent type into a
+    dpctl.SyclEvent PyObject
+
+    At this point numba-dpex does not support creating a dpctl.SyclEvent inside
+    a dpjit decorated function. For this reason, boxing is only returns the
+    original parent object stored in DpctlSyclEvent's data model.
+
+    Args:
+        typ: The representation of the dpctl.SyclEvent type.
+        val: A native representation of a Numba DpctlSyclEvent type object.
+        c: The boxing context.
+
+    Returns: A Pyobject for a dpctl.SyclEvent boxed from the Numba native value.
+    """
+
+    if not c.context.enable_nrt:
+        raise UnreachableError
+
+    dpexrtCtx = dpexrt.DpexRTContext(c.context)
+    event = dpexrtCtx.eventstruct_to_python(c.pyapi, val)
+
+    if not event:
+        c.pyapi.err_set_string(
+            "PyExc_TypeError",
+            "could not box native sycl queue into a dpctl.SyclEvent"
+            " PyObject.",
+        )
+    return event
diff --git a/numba_dpex/core/types/dpnp_ndarray_type.py b/numba_dpex/core/types/dpnp_ndarray_type.py
index 9fc32c909c..34aefb653f 100644
--- a/numba_dpex/core/types/dpnp_ndarray_type.py
+++ b/numba_dpex/core/types/dpnp_ndarray_type.py
@@ -1,8 +1,7 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-
 import dpnp
 from numba.core import cgutils, ir, types
 from numba.core.errors import NumbaNotImplementedError
@@ -200,19 +199,22 @@ def __allocate__(
         return out
 
 
+# TODO: move this section to separate file
 # --------------- Boxing/Unboxing logic for dpnp.ndarray ----------------------#
 
 
-@unbox(DpnpNdArray)
+@unbox(USMNdArray)
 def unbox_dpnp_nd_array(typ, obj, c):
-    """Converts a dpnp.ndarray object to a Numba internal array structure.
+    """Converts a dpctl.tensor.usm_ndarray/dpnp.ndarray object to a Numba-dpex
+    internal array structure.
 
     Args:
         typ : The Numba type of the PyObject
         obj : The actual PyObject to be unboxed
         c : The unboxing context
 
-    Returns: A NativeValue object representing an unboxed dpnp.ndarray
+    Returns: A NativeValue object representing an unboxed
+        dpctl.tensor.usm_ndarray/dpnp.ndarray
     """
     # Reusing the numba.core.base.BaseContext's make_array function to get a
     # struct allocated. The same struct is used for numpy.ndarray
@@ -264,24 +266,26 @@ def unbox_dpnp_nd_array(typ, obj, c):
     with c.builder.if_then(failed, likely=False):
         c.pyapi.err_set_string(
             "PyExc_TypeError",
-            "can't unbox array from PyObject into "
+            "can't unbox usm array from PyObject into "
             "native value.  The object maybe of a "
             "different type",
         )
     return NativeValue(c.builder.load(aryptr), is_error=failed)
 
 
-@box(DpnpNdArray)
+@box(USMNdArray)
 def box_array(typ, val, c):
-    """Boxes a NativeValue representation of DpnpNdArray type into a
-    dpnp.ndarray PyObject
+    """Boxes a NativeValue representation of USMNdArray/DpnpNdArray type into a
+    dpctl.tensor.usm_ndarray/dpnp.ndarray PyObject
 
     Args:
-        typ: The representation of the DpnpNdArray type.
-        val: A native representation of a Numba DpnpNdArray type object.
+        typ: The representation of the USMNdArray/DpnpNdArray type.
+        val: A native representation of a Numba USMNdArray/DpnpNdArray type
+            object.
         c: The boxing context.
 
-    Returns: A Pyobject for a dpnp.ndarray boxed from the Numba native value.
+    Returns: A Pyobject for a dpctl.tensor.usm_ndarray/dpnp.ndarray boxed from
+        the Numba-dpex native value.
     """
     if c.context.enable_nrt:
         np_dtype = numpy_support.as_dtype(typ.dtype)
diff --git a/numba_dpex/core/types/numba_types_short_names.py b/numba_dpex/core/types/numba_types_short_names.py
index d477860b49..53a9c0bbc1 100644
--- a/numba_dpex/core/types/numba_types_short_names.py
+++ b/numba_dpex/core/types/numba_types_short_names.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/types/range_types.py b/numba_dpex/core/types/range_types.py
new file mode 100644
index 0000000000..c88cd54294
--- /dev/null
+++ b/numba_dpex/core/types/range_types.py
@@ -0,0 +1,262 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from contextlib import ExitStack
+
+from numba.core import cgutils, errors, types
+from numba.core.datamodel import default_manager
+from numba.extending import NativeValue, box, unbox
+
+from ..kernel_interface.indexers import NdRange, Range
+
+
+class RangeType(types.Type):
+    """Numba-dpex type corresponding to
+    :class:`numba_dpex.core.kernel_interface.indexers.Range`
+    """
+
+    def __init__(self, ndim: int):
+        self._ndim = ndim
+        if ndim < 1 or ndim > 3:
+            raise errors.TypingError(
+                "RangeType can only have 1,2, or 3 dimensions"
+            )
+        super(RangeType, self).__init__(name="Range<" + str(ndim) + ">")
+
+    @property
+    def ndim(self):
+        return self._ndim
+
+    @property
+    def key(self):
+        return self._ndim
+
+
+class NdRangeType(types.Type):
+    """Numba-dpex type corresponding to
+    :class:`numba_dpex.core.kernel_interface.indexers.NdRange`
+    """
+
+    def __init__(self, ndim: int):
+        self._ndim = ndim
+        if ndim < 1 or ndim > 3:
+            raise errors.TypingError(
+                "RangeType can only have 1,2, or 3 dimensions"
+            )
+        super(NdRangeType, self).__init__(name="NdRange<" + str(ndim) + ">")
+
+    @property
+    def ndim(self):
+        return self._ndim
+
+    @property
+    def key(self):
+        return self._ndim
+
+
+@unbox(RangeType)
+def unbox_range(typ, obj, c):
+    """
+    Converts a Python Range object to numba-dpex's native struct representation
+    for RangeType.
+    """
+    is_error_ptr = cgutils.alloca_once_value(c.builder, cgutils.false_bit)
+    range_struct = cgutils.create_struct_proxy(typ)(c.context, c.builder)
+    with ExitStack() as stack:
+        range_attr_native_value_map = {
+            "ndim": None,
+            "dim0": None,
+            "dim1": None,
+            "dim2": None,
+        }
+
+        for attr in range_attr_native_value_map.keys():
+            attr_obj = c.pyapi.object_getattr_string(obj, attr)
+            with cgutils.early_exit_if_null(c.builder, stack, attr_obj):
+                c.builder.store(cgutils.true_bit, is_error_ptr)
+            attr_native = c.unbox(types.int64, attr_obj)
+            c.pyapi.decref(attr_obj)
+            with cgutils.early_exit_if(c.builder, stack, attr_native.is_error):
+                c.builder.store(cgutils.true_bit, is_error_ptr)
+            range_attr_native_value_map[attr] = attr_native
+
+        range_struct.ndim = range_attr_native_value_map["ndim"].value
+        range_struct.dim0 = range_attr_native_value_map["dim0"].value
+        range_struct.dim1 = range_attr_native_value_map["dim1"].value
+        range_struct.dim2 = range_attr_native_value_map["dim2"].value
+
+    return NativeValue(
+        range_struct._getvalue(), is_error=c.builder.load(is_error_ptr)
+    )
+
+
+@unbox(NdRangeType)
+def unbox_ndrange(typ, obj, c):
+    """
+    Converts a Python Range object to numba-dpex's native struct representation
+    for NdRangeType.
+    """
+    is_error_ptr = cgutils.alloca_once_value(c.builder, cgutils.false_bit)
+    ndrange_struct = cgutils.create_struct_proxy(typ)(c.context, c.builder)
+
+    with ExitStack() as stack:
+        ndrange_attr_native_value_map = {
+            "global_range": None,
+            "local_range": None,
+        }
+
+        for attr in ndrange_attr_native_value_map.keys():
+            attr_obj = c.pyapi.object_getattr_string(obj, attr)
+            with cgutils.early_exit_if_null(c.builder, stack, attr_obj):
+                c.builder.store(cgutils.true_bit, is_error_ptr)
+            attr_native = c.unbox(RangeType(typ.ndim), attr_obj)
+            c.pyapi.decref(attr_obj)
+            with cgutils.early_exit_if(c.builder, stack, attr_native.is_error):
+                c.builder.store(cgutils.true_bit, is_error_ptr)
+            ndrange_attr_native_value_map[attr] = attr_native
+
+        global_range_struct = ndrange_attr_native_value_map[
+            "global_range"
+        ].value
+        local_range_struct = ndrange_attr_native_value_map["local_range"].value
+
+        range_datamodel = default_manager.lookup(RangeType(typ.ndim))
+        ndrange_struct.ndim = c.builder.extract_value(
+            global_range_struct,
+            range_datamodel.get_field_position("ndim"),
+        )
+        ndrange_struct.gdim0 = c.builder.extract_value(
+            global_range_struct,
+            range_datamodel.get_field_position("dim0"),
+        )
+        ndrange_struct.gdim1 = c.builder.extract_value(
+            global_range_struct,
+            range_datamodel.get_field_position("dim1"),
+        )
+        ndrange_struct.gdim2 = c.builder.extract_value(
+            global_range_struct,
+            range_datamodel.get_field_position("dim2"),
+        )
+        ndrange_struct.ldim0 = c.builder.extract_value(
+            local_range_struct,
+            range_datamodel.get_field_position("dim0"),
+        )
+        ndrange_struct.ldim1 = c.builder.extract_value(
+            local_range_struct,
+            range_datamodel.get_field_position("dim1"),
+        )
+        ndrange_struct.ldim2 = c.builder.extract_value(
+            local_range_struct,
+            range_datamodel.get_field_position("dim2"),
+        )
+
+    return NativeValue(
+        ndrange_struct._getvalue(), is_error=c.builder.load(is_error_ptr)
+    )
+
+
+@box(RangeType)
+def box_range(typ, val, c):
+    """
+    Convert a native range structure to a Range object.
+    """
+    ret_ptr = cgutils.alloca_once(c.builder, c.pyapi.pyobj)
+    fail_obj = c.pyapi.get_null_object()
+
+    with ExitStack() as stack:
+        range_struct = cgutils.create_struct_proxy(typ)(
+            c.context, c.builder, value=val
+        )
+
+        dim0_obj = c.box(types.int64, range_struct.dim0)
+        with cgutils.early_exit_if_null(c.builder, stack, dim0_obj):
+            c.builder.store(fail_obj, ret_ptr)
+        dim1_obj = c.box(types.int64, range_struct.dim1)
+        with cgutils.early_exit_if_null(c.builder, stack, dim1_obj):
+            c.builder.store(fail_obj, ret_ptr)
+        dim2_obj = c.box(types.int64, range_struct.dim2)
+        with cgutils.early_exit_if_null(c.builder, stack, dim2_obj):
+            c.builder.store(fail_obj, ret_ptr)
+
+        class_obj = c.pyapi.unserialize(c.pyapi.serialize_object(Range))
+        with cgutils.early_exit_if_null(c.builder, stack, class_obj):
+            c.pyapi.decref(dim0_obj)
+            c.pyapi.decref(dim1_obj)
+            c.pyapi.decref(dim2_obj)
+            c.builder.store(fail_obj, ret_ptr)
+        # NOTE: The result of this call is not checked as the clean up
+        # has to occur regardless of whether it is successful. If it
+        # fails `res` is set to NULL and a Python exception is set.
+        if typ.ndim == 1:
+            res = c.pyapi.call_function_objargs(class_obj, (dim0_obj,))
+        elif typ.ndim == 2:
+            res = c.pyapi.call_function_objargs(class_obj, (dim0_obj, dim1_obj))
+        elif typ.ndim == 3:
+            res = c.pyapi.call_function_objargs(
+                class_obj, (dim0_obj, dim1_obj, dim2_obj)
+            )
+        else:
+            raise ValueError("Cannot unbox Range instance.")
+
+        c.pyapi.decref(dim0_obj)
+        c.pyapi.decref(dim1_obj)
+        c.pyapi.decref(dim2_obj)
+        c.pyapi.decref(class_obj)
+        c.builder.store(res, ret_ptr)
+
+    return c.builder.load(ret_ptr)
+
+
+@box(NdRangeType)
+def box_ndrange(typ, val, c):
+    """
+    Convert a native range structure to a Range object.
+    """
+    ret_ptr = cgutils.alloca_once(c.builder, c.pyapi.pyobj)
+    fail_obj = c.pyapi.get_null_object()
+
+    with ExitStack() as stack:
+        ndrange_struct = cgutils.create_struct_proxy(typ)(
+            c.context, c.builder, value=val
+        )
+        grange_struct = cgutils.create_struct_proxy(RangeType(typ.ndim))(
+            c.context, c.builder
+        )
+        lrange_struct = cgutils.create_struct_proxy(RangeType(typ.ndim))(
+            c.context, c.builder
+        )
+        grange_struct.ndim = ndrange_struct.ndim
+        grange_struct.dim0 = ndrange_struct.gdim0
+        grange_struct.dim1 = ndrange_struct.gdim1
+        grange_struct.dim2 = ndrange_struct.gdim2
+        lrange_struct.dim0 = ndrange_struct.ldim0
+        lrange_struct.dim1 = ndrange_struct.ldim1
+        lrange_struct.dim2 = ndrange_struct.ldim2
+
+        grange_obj = c.box(RangeType(typ.ndim), grange_struct._getvalue())
+        with cgutils.early_exit_if_null(c.builder, stack, grange_obj):
+            c.builder.store(fail_obj, ret_ptr)
+
+        lrange_obj = c.box(RangeType(typ.ndim), lrange_struct._getvalue())
+        with cgutils.early_exit_if_null(c.builder, stack, lrange_obj):
+            c.builder.store(fail_obj, ret_ptr)
+
+        class_obj = c.pyapi.unserialize(c.pyapi.serialize_object(NdRange))
+        with cgutils.early_exit_if_null(c.builder, stack, class_obj):
+            c.pyapi.decref(grange_obj)
+            c.pyapi.decref(lrange_obj)
+            c.builder.store(fail_obj, ret_ptr)
+
+        # NOTE: The result of this call is not checked as the clean up
+        # has to occur regardless of whether it is successful. If it
+        # fails `res` is set to NULL and a Python exception is set.
+
+        res = c.pyapi.call_function_objargs(class_obj, (grange_obj, lrange_obj))
+
+        c.pyapi.decref(grange_obj)
+        c.pyapi.decref(lrange_obj)
+        c.pyapi.decref(class_obj)
+        c.builder.store(res, ret_ptr)
+
+    return c.builder.load(ret_ptr)
diff --git a/numba_dpex/core/types/usm_ndarray_type.py b/numba_dpex/core/types/usm_ndarray_type.py
index e252adf58d..79e6c09fc4 100644
--- a/numba_dpex/core/types/usm_ndarray_type.py
+++ b/numba_dpex/core/types/usm_ndarray_type.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -13,7 +13,7 @@
 from numba.np.numpy_support import from_dtype
 
 from numba_dpex.core.types.dpctl_types import DpctlSyclQueue
-from numba_dpex.utils import address_space
+from numba_dpex.utils.constants import address_space
 
 
 class USMNdArray(Array):
diff --git a/numba_dpex/core/typing/__init__.py b/numba_dpex/core/typing/__init__.py
index 0c13949a42..bf9cdde574 100644
--- a/numba_dpex/core/typing/__init__.py
+++ b/numba_dpex/core/typing/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/typing/dpnpdecl.py b/numba_dpex/core/typing/dpnpdecl.py
index 7c8cf817b5..355be22179 100644
--- a/numba_dpex/core/typing/dpnpdecl.py
+++ b/numba_dpex/core/typing/dpnpdecl.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -10,8 +10,24 @@
     NumpyRulesArrayOperator,
     NumpyRulesInplaceArrayOperator,
     NumpyRulesUnaryArrayOperator,
-    infer_global,
 )
+from numba.core.typing.templates import Registry
+
+registry = Registry()
+infer = registry.register
+infer_global = registry.register_global
+infer_getattr = registry.register_attr
+
+
+def _install_operations(cls: NumpyRulesArrayOperator):
+    for op, ufunc_name in cls._op_map.items():
+        infer_global(op)(
+            type(
+                "NumpyRulesArrayOperator_" + ufunc_name,
+                (cls,),
+                dict(key=op),
+            )
+        )
 
 
 class DpnpRulesArrayOperator(NumpyRulesArrayOperator):
@@ -29,13 +45,21 @@ def ufunc(self):
         except:
             pass
 
+    @classmethod
+    def install_operations(cls):
+        _install_operations(cls)
+
 
 class DpnpRulesInplaceArrayOperator(NumpyRulesInplaceArrayOperator):
-    pass
+    @classmethod
+    def install_operations(cls):
+        _install_operations(cls)
 
 
 class DpnpRulesUnaryArrayOperator(NumpyRulesUnaryArrayOperator):
-    pass
+    @classmethod
+    def install_operations(cls):
+        _install_operations(cls)
 
 
 # list of unary ufuncs to register
diff --git a/numba_dpex/core/typing/typeof.py b/numba_dpex/core/typing/typeof.py
index 862a350645..cce8cc78ba 100644
--- a/numba_dpex/core/typing/typeof.py
+++ b/numba_dpex/core/typing/typeof.py
@@ -1,17 +1,19 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-from dpctl import SyclQueue
+from dpctl import SyclEvent, SyclQueue
 from dpctl.tensor import usm_ndarray
 from dpnp import ndarray
 from numba.extending import typeof_impl
 from numba.np import numpy_support
 
-from numba_dpex.utils import address_space
+from numba_dpex.utils.constants import address_space
 
-from ..types.dpctl_types import DpctlSyclQueue
+from ..kernel_interface.indexers import NdRange, Range
+from ..types.dpctl_types import DpctlSyclEvent, DpctlSyclQueue
 from ..types.dpnp_ndarray_type import DpnpNdArray
+from ..types.range_types import NdRangeType, RangeType
 from ..types.usm_ndarray_type import USMNdArray
 
 
@@ -106,3 +108,45 @@ def typeof_dpctl_sycl_queue(val, c):
     Returns: A numba_dpex.core.types.dpctl_types.DpctlSyclQueue instance.
     """
     return DpctlSyclQueue(val)
+
+
+@typeof_impl.register(SyclEvent)
+def typeof_dpctl_sycl_event(val, c):
+    """Registers the type inference implementation function for a
+    dpctl.SyclEvent PyObject.
+
+    Args:
+        val : An instance of dpctl.SyclEvent.
+        c : Unused argument used to be consistent with Numba API.
+
+    Returns: A numba_dpex.core.types.dpctl_types.DpctlSyclEvent instance.
+    """
+    return DpctlSyclEvent()
+
+
+@typeof_impl.register(Range)
+def typeof_range(val, c):
+    """Registers the type inference implementation function for a
+    numba_dpex.Range PyObject.
+
+    Args:
+        val : An instance of numba_dpex.Range.
+        c : Unused argument used to be consistent with Numba API.
+
+    Returns: A numba_dpex.core.types.range_types.RangeType instance.
+    """
+    return RangeType(val.ndim)
+
+
+@typeof_impl.register(NdRange)
+def typeof_ndrange(val, c):
+    """Registers the type inference implementation function for a
+    numba_dpex.NdRange PyObject.
+
+    Args:
+        val : An instance of numba_dpex.Range.
+        c : Unused argument used to be consistent with Numba API.
+
+    Returns: A numba_dpex.core.types.range_types.RangeType instance.
+    """
+    return NdRangeType(val.global_range.ndim)
diff --git a/numba_dpex/core/utils/__init__.py b/numba_dpex/core/utils/__init__.py
index 6ba7c29540..c842a1c9b6 100644
--- a/numba_dpex/core/utils/__init__.py
+++ b/numba_dpex/core/utils/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/utils/caching_utils.py b/numba_dpex/core/utils/caching_utils.py
index 9d595fb174..f02d883d96 100644
--- a/numba_dpex/core/utils/caching_utils.py
+++ b/numba_dpex/core/utils/caching_utils.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/utils/kernel_launcher.py b/numba_dpex/core/utils/kernel_launcher.py
index 5865160c6f..4acc4da8b8 100644
--- a/numba_dpex/core/utils/kernel_launcher.py
+++ b/numba_dpex/core/utils/kernel_launcher.py
@@ -1,13 +1,97 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
+"""Module that contains numba style wrapper around sycl kernel submit."""
+
+from dataclasses import dataclass
+from functools import cached_property
+from typing import NamedTuple, Union
+
+import dpctl
+from llvmlite import ir as llvmir
+from llvmlite.ir.builder import IRBuilder
 from numba.core import cgutils, types
+from numba.core.cpu import CPUContext
+from numba.core.datamodel import DataModelManager
+from numba.core.types.containers import UniTuple
 
 from numba_dpex import utils
+from numba_dpex.core import config
+from numba_dpex.core.exceptions import UnreachableError
 from numba_dpex.core.runtime.context import DpexRTContext
-from numba_dpex.dpctl_iface import DpctlCAPIFnBuilder
+from numba_dpex.core.types import USMNdArray
+from numba_dpex.core.types.range_types import NdRangeType, RangeType
+from numba_dpex.dpctl_iface import libsyclinterface_bindings as sycl
 from numba_dpex.dpctl_iface._helpers import numba_type_to_dpctl_typenum
+from numba_dpex.utils import create_null_ptr
+
+MAX_SIZE_OF_SYCL_RANGE = 3
+
+
+# TODO: probably not best place for it. Should be in kernel_dispatcher once we
+# get merge experimental. Right now it will cause cyclic import
+class SPIRVKernelModule(NamedTuple):
+    """Represents SPIRV binary code and function name in this binary"""
+
+    kernel_name: str
+    kernel_bitcode: bytes
+
+
+@dataclass
+class _KernelLaunchIRArguments:  # pylint: disable=too-many-instance-attributes
+    """List of kernel launch arguments used in sycl.dpctl_queue_submit_range and
+    sycl.dpctl_queue_submit_ndrange."""
+
+    sycl_kernel_ref: llvmir.Instruction = None
+    sycl_queue_ref: llvmir.Instruction = None
+    arg_list: llvmir.Instruction = None
+    arg_ty_list: llvmir.Instruction = None
+    total_kernel_args: llvmir.Instruction = None
+    global_range: llvmir.Instruction = None
+    local_range: llvmir.Instruction = None
+    range_size: llvmir.Instruction = None
+    dep_events: llvmir.Instruction = None
+    dep_events_len: llvmir.Instruction = None
+
+    def to_list(self):
+        """Returns list of arguments in the right order to pass to
+        sycl.dpctl_queue_submit_range or sycl.dpctl_queue_submit_ndrange."""
+        res = [
+            self.sycl_kernel_ref,
+            self.sycl_queue_ref,
+            self.arg_list,
+            self.arg_ty_list,
+            self.total_kernel_args,
+            self.global_range,
+        ]
+
+        if self.local_range is not None:
+            res += [self.local_range]
+
+        res += [
+            self.range_size,
+            self.dep_events,
+            self.dep_events_len,
+        ]
+
+        return res
+
+
+@dataclass
+class _KernelLaunchIRCachedArguments:
+    """Arguments that are being used in KernelLaunchIRBuilder that are either
+    intermediate structure of the KernelLaunchIRBuilder like llvm IR array
+    stored as a python array of llvm IR values or llvm IR values that may be
+    used as an input for builder functions.
+
+    Main goal is to prevent passing same argument during build process several
+    times and to avoid passing output of the builder as an argument for another
+    build method."""
+
+    arg_list: list[llvmir.Instruction] = None
+    arg_ty_list: list[types.Type] = None
+    device_event_ref: llvmir.Instruction = None
 
 
 class KernelLaunchIRBuilder:
@@ -19,20 +103,31 @@ class KernelLaunchIRBuilder:
     for submitting kernels. The LLVM Values that
     """
 
-    def __init__(self, lowerer, kernel):
+    def __init__(
+        self,
+        context: CPUContext,
+        builder: IRBuilder,
+        kernel_dmm: DataModelManager,
+    ):
         """Create a KernelLauncher for the specified kernel.
 
         Args:
-            lowerer: The Numba Lowerer that will be used to generate the code.
-            kernel: The SYCL kernel for which we are generating the code.
-            num_inputs: The number of arguments to the kernels.
+            context: A Numba target context that will be used to generate the
+                     code.
+            builder: An llvmlite IRBuilder instance used to generate LLVM IR.
         """
-        self.lowerer = lowerer
-        self.context = self.lowerer.context
-        self.builder = self.lowerer.builder
-        self.kernel = kernel
-        self.kernel_addr = self.kernel.addressof_ref()
-        self.rtctx = DpexRTContext(self.context)
+        self.context = context
+        self.builder = builder
+        self.arguments = _KernelLaunchIRArguments()
+        self.cached_arguments = _KernelLaunchIRCachedArguments()
+        self.kernel_dmm = kernel_dmm
+        self._cleanups = []
+
+    @cached_property
+    def dpexrt(self):
+        """Dpex runtime context."""
+
+        return DpexRTContext(self.context)
 
     def _build_nullptr(self):
         """Builds the LLVM IR to represent a null pointer.
@@ -45,7 +140,7 @@ def _build_nullptr(self):
             zero, utils.get_llvm_type(context=self.context, type=types.voidptr)
         )
 
-    def _build_array_attr_arg(
+    def _build_array_attr_arg(  # pylint: disable=too-many-arguments
         self,
         array_val,
         array_attr_pos,
@@ -69,15 +164,15 @@ def _build_array_attr_arg(
         ):
             array_attr = self.builder.load(array_attr)
 
-        self.build_arg(
+        self._build_arg(
             val=array_attr,
-            ty=array_attr_ty,
+            typ=array_attr_ty,
             arg_list=arg_list,
             args_ty_list=args_ty_list,
             arg_num=arg_num,
         )
 
-    def _build_unituple_member_arg(
+    def _build_unituple_member_arg(  # pylint: disable=too-many-arguments
         self, array_val, array_attr_pos, ndims, arg_list, args_ty_list, arg_num
     ):
         array_attr = self.builder.gep(
@@ -98,13 +193,15 @@ def _build_unituple_member_arg(
                 arg_num=arg_num + ndim,
             )
 
-    def build_arg(self, val, ty, arg_list, args_ty_list, arg_num):
+    def _build_arg(
+        self, val, typ, arg_list, args_ty_list, arg_num
+    ):  # pylint: disable=too-many-arguments
         """Stores the kernel arguments and the kernel argument types into
         arrays that will be passed to DPCTLQueue_SubmitRange.
 
         Args:
             val: An LLVM IR Value that will be stored into the arguments array
-            ty: A Numba type that will be converted to a DPCTLKernelArgType
+            typ: A Numba type that will be converted to a DPCTLKernelArgType
             enum and stored into the argument types list array
             arg_list: An LLVM IR Value array that stores the kernel arguments
             args_ty_list: An LLVM IR Value array that stores the
@@ -126,17 +223,19 @@ def build_arg(self, val, ty, arg_list, args_ty_list, arg_num):
         )
         self.builder.store(val, kernel_arg_dst)
         self.builder.store(
-            numba_type_to_dpctl_typenum(self.context, ty), kernel_arg_ty_dst
+            numba_type_to_dpctl_typenum(self.context, typ), kernel_arg_ty_dst
         )
 
-    def build_complex_arg(self, val, ty, arg_list, args_ty_list, arg_num):
+    def _build_complex_arg(
+        self, val, typ, arg_list, args_ty_list, arg_num
+    ):  # pylint: disable=too-many-arguments
         """Creates a list of LLVM Values for an unpacked complex kernel
         argument.
         """
         self._build_array_attr_arg(
             array_val=val,
             array_attr_pos=0,
-            array_attr_ty=ty,
+            array_attr_ty=typ,
             arg_list=arg_list,
             args_ty_list=args_ty_list,
             arg_num=arg_num,
@@ -145,53 +244,38 @@ def build_complex_arg(self, val, ty, arg_list, args_ty_list, arg_num):
         self._build_array_attr_arg(
             array_val=val,
             array_attr_pos=1,
-            array_attr_ty=ty,
+            array_attr_ty=typ,
             arg_list=arg_list,
             args_ty_list=args_ty_list,
             arg_num=arg_num,
         )
         arg_num += 1
 
-    def build_array_arg(
+    def _build_array_arg(  # pylint: disable=too-many-arguments
         self,
-        array_val,
-        array_data_model,
-        array_rank,
-        arg_list,
-        args_ty_list,
+        host_array_val,  # llvm_val
+        host_array_data_model,
+        kernel_array_data_model,
+        arg_list,  # filling this val
+        args_ty_list,  # filling this val
         arg_num,
     ):
-        """Creates a list of LLVM Values for an unpacked DpnpNdArray kernel
+        """Creates a list of LLVM Values for an unpacked USMNdArray kernel
         argument.
 
         The steps performed here are the same as in
         numba_dpex.core.kernel_interface.arg_pack_unpacker._unpack_array_helper
         """
-        # Argument 1: Null pointer for the NRT_MemInfo attribute of the array
-        nullptr = self._build_nullptr()
-        self.build_arg(
-            val=nullptr,
-            ty=types.int64,
-            arg_list=arg_list,
-            args_ty_list=args_ty_list,
-            arg_num=arg_num,
-        )
-        arg_num += 1
-        # Argument 2: Null pointer for the Parent attribute of the array
-        nullptr = self._build_nullptr()
-        self.build_arg(
-            val=nullptr,
-            ty=types.int64,
-            arg_list=arg_list,
-            args_ty_list=args_ty_list,
-            arg_num=arg_num,
-        )
-        arg_num += 1
+        # It might be quite confusing, but we are referring to field position
+        # on host device to cast it then to device type and send to device.vars
+        # Keeping that in mind answers when to use what data_model.
+        # Proper code structure refactoring needed to make it easier to read and
+        # maintain.
         # Argument nitems
         self._build_array_attr_arg(
-            array_val=array_val,
-            array_attr_pos=array_data_model.get_field_position("nitems"),
-            array_attr_ty=array_data_model.get_member_fe_type("nitems"),
+            array_val=host_array_val,
+            array_attr_pos=host_array_data_model.get_field_position("nitems"),
+            array_attr_ty=kernel_array_data_model.get_member_fe_type("nitems"),
             arg_list=arg_list,
             args_ty_list=args_ty_list,
             arg_num=arg_num,
@@ -199,9 +283,11 @@ def build_array_arg(
         arg_num += 1
         # Argument itemsize
         self._build_array_attr_arg(
-            array_val=array_val,
-            array_attr_pos=array_data_model.get_field_position("itemsize"),
-            array_attr_ty=array_data_model.get_member_fe_type("itemsize"),
+            array_val=host_array_val,
+            array_attr_pos=host_array_data_model.get_field_position("itemsize"),
+            array_attr_ty=kernel_array_data_model.get_member_fe_type(
+                "itemsize"
+            ),
             arg_list=arg_list,
             args_ty_list=args_ty_list,
             arg_num=arg_num,
@@ -209,29 +295,19 @@ def build_array_arg(
         arg_num += 1
         # Argument data
         self._build_array_attr_arg(
-            array_val=array_val,
-            array_attr_pos=array_data_model.get_field_position("data"),
-            array_attr_ty=array_data_model.get_member_fe_type("data"),
-            arg_list=arg_list,
-            args_ty_list=args_ty_list,
-            arg_num=arg_num,
-        )
-        arg_num += 1
-        # Argument sycl_queue: as the queue pointer is not to be used in a
-        # kernel we always pass in a nullptr
-        self.build_arg(
-            val=nullptr,
-            ty=types.int64,
+            array_val=host_array_val,
+            array_attr_pos=host_array_data_model.get_field_position("data"),
+            array_attr_ty=kernel_array_data_model.get_member_fe_type("data"),
             arg_list=arg_list,
             args_ty_list=args_ty_list,
             arg_num=arg_num,
         )
         arg_num += 1
         # Arguments for shape
-        shape_member = array_data_model.get_member_fe_type("shape")
+        shape_member = kernel_array_data_model.get_member_fe_type("shape")
         self._build_unituple_member_arg(
-            array_val=array_val,
-            array_attr_pos=array_data_model.get_field_position("shape"),
+            array_val=host_array_val,
+            array_attr_pos=host_array_data_model.get_field_position("shape"),
             ndims=shape_member.count,
             arg_list=arg_list,
             args_ty_list=args_ty_list,
@@ -239,10 +315,10 @@ def build_array_arg(
         )
         arg_num += shape_member.count
         # Arguments for strides
-        stride_member = array_data_model.get_member_fe_type("strides")
+        stride_member = kernel_array_data_model.get_member_fe_type("strides")
         self._build_unituple_member_arg(
-            array_val=array_val,
-            array_attr_pos=array_data_model.get_field_position("strides"),
+            array_val=host_array_val,
+            array_attr_pos=host_array_data_model.get_field_position("strides"),
             ndims=stride_member.count,
             arg_list=arg_list,
             args_ty_list=args_ty_list,
@@ -250,7 +326,8 @@ def build_array_arg(
         )
         arg_num += stride_member.count
 
-    def get_queue(self, exec_queue):
+    # TODO: remove, not part of the builder
+    def get_queue(self, exec_queue: dpctl.SyclQueue) -> llvmir.Instruction:
         """Allocates memory on the stack to store a DPCTLSyclQueueRef.
 
         Returns: A LLVM Value storing the pointer to the SYCL queue created
@@ -269,136 +346,518 @@ def get_queue(self, exec_queue):
         # Store the queue returned by DPEXRTQueue_CreateFromFilterString in a
         # local variable
         self.builder.store(
-            self.rtctx.get_queue_from_filter_string(
+            self.dpexrt.get_queue_from_filter_string(
                 builder=self.builder, device=device
             ),
             sycl_queue_val,
         )
-        return sycl_queue_val
+        return self.builder.load(sycl_queue_val)
 
-    def free_queue(self, sycl_queue_val):
-        """
-        Frees the ``DPCTLSyclQueueRef`` pointer that was used to launch the
-        kernels.
+    def _allocate_array(
+        self, numba_type: types.Type, size: int
+    ) -> llvmir.Instruction:
+        """Allocates an LLVM array of given type and size.
 
         Args:
-            sycl_queue_val: The SYCL queue pointer to be freed.
+            numba_type: type of the array to allocate,
+            size: The size of the array to allocate.
+
+        Returns: An LLVM IR value pointing to the array.
         """
-        fn = DpctlCAPIFnBuilder.get_dpctl_queue_delete(
-            builder=self.builder, context=self.context
+        return cgutils.alloca_once(
+            self.builder,
+            self.context.get_value_type(numba_type),
+            size=self.context.get_constant(types.uintp, size),
         )
-        self.builder.call(fn, [self.builder.load(sycl_queue_val)])
 
-    def allocate_kernel_arg_array(self, num_kernel_args):
-        """Allocates an array to store the LLVM Value for every kernel argument.
+    def _populate_array_from_python_list(
+        self,
+        numba_type: types.Type,
+        py_array: list[llvmir.Instruction],
+        ll_array: llvmir.Instruction,
+        force_cast: bool = False,
+    ):
+        """Populates LLVM values from an input Python list into an LLVM array.
 
         Args:
-            num_kernel_args (int): The number of kernel arguments that
-            determines the size of args array to allocate.
+            numba_type: type of the array to allocate,
+            py_array: array of llvm ir values to populate.
+            ll_array: llvm ir value that represents an array to populate,
+            force_cast: either force cast values to the provided type.
+        """
+        for idx, ll_value in enumerate(py_array):
+            ll_array_dst = self.builder.gep(
+                ll_array,
+                [self.context.get_constant(types.int32, idx)],
+            )
+            # bitcast may be extra, but won't hurt,
+            if force_cast:
+                ll_value = self.builder.bitcast(
+                    ll_value,
+                    self.context.get_value_type(numba_type),
+                )
+            self.builder.store(ll_value, ll_array_dst)
+
+    def _create_ll_from_py_list(
+        self,
+        numba_type: types.Type,
+        list_of_ll_values: list[llvmir.Instruction],
+        force_cast: bool = False,
+    ) -> llvmir.Instruction:
+        """Allocates an LLVM IR array of the same size as the input python list
+        of LLVM IR Values and populates the array with the LLVM Values in the
+        list.
+
+        Args:
+            numba_type: type of the array to allocate,
+            list_of_ll_values: list of LLVM IR values to populate,
+            force_cast: either force cast values to the provided type.
 
-        Returns: An LLVM IR value pointing to an array to store the kernel
-        arguments.
+        Returns: An LLVM IR value pointing to the array.
         """
-        args_list = cgutils.alloca_once(
-            self.builder,
-            utils.get_llvm_type(context=self.context, type=types.voidptr),
-            size=self.context.get_constant(types.uintp, num_kernel_args),
+        ll_array = self._allocate_array(numba_type, len(list_of_ll_values))
+        self._populate_array_from_python_list(
+            numba_type, list_of_ll_values, ll_array, force_cast
         )
 
-        return args_list
+        return ll_array
 
-    def allocate_kernel_arg_ty_array(self, num_kernel_args):
-        """Allocates an array to store the LLVM Value for the typenum for
-        every kernel argument.
+    def _create_sycl_range(self, idx_range):
+        """Allocate an array to store the extents of a sycl::range.
 
-        Args:
-            num_kernel_args (int): The number of kernel arguments that
-            determines the size of args array to allocate.
+        Sycl supports upto 3-dimensional ranges and a such the array is
+        statically sized to length three. Only the elements that store an actual
+        range value are populated based on the size of the idx_range argument.
 
-        Returns: An LLVM IR value pointing to an array to store the kernel
-        arguments typenums as defined in dpctl.
         """
-        args_ty_list = cgutils.alloca_once(
+        int64_range = [
+            self.builder.sext(rext, utils.LLVMTypes.int64_t)
+            if rext.type != utils.LLVMTypes.int64_t
+            else rext
+            for rext in idx_range
+        ]
+
+        # Index inversion is done here as numba-dpex first compiles a native
+        # kernel (OpenCL or Level Zero) and then generates a SYCL
+        # interoperability kernel from it. The convention for unit stride
+        # dimensions is opposite for OpenCL and SYCL
+        # refer:
+        # https://registry.khronos.org/SYCL/specs/sycl-2020/html/sycl-2020.html#sec:opencl:kernel-conventions-sycl
+        # For this reason, although numba-dpex follows SYCL like indexing in
+        # the kernel front-end while launching the kernel the indexing is
+        # reversed.
+        #
+        # TODO[1]: It needs to be investigated if we need the index reversal
+        # if we use SYCL-like LLVM IR indexing intrinsic instead of
+        # OpenCL-like LLVM IR intrinsic functions.
+        #
+        # TODO[2]: Do we need to do this when the backend is LevelZero
+        int64_range.reverse()
+
+        return self._create_ll_from_py_list(types.uintp, int64_range)
+
+    def set_kernel(self, sycl_kernel_ref: llvmir.Instruction):
+        """Sets kernel to the argument list."""
+        self.arguments.sycl_kernel_ref = sycl_kernel_ref
+
+    def set_kernel_from_spirv(self, kernel_module: SPIRVKernelModule):
+        """Sets kernel to the argument list from the SPIRV bytecode.
+
+        It pastes bytecode as a constant string and create kernel bundle from it
+        using SYCL API. It caches kernel, so it won't be sent to device second
+        time.
+        """
+        # Inserts a global constant byte string in the current LLVM module to
+        # store the passed in SPIR-V binary blob.
+        queue_ref = self.arguments.sycl_queue_ref
+
+        kernel_bc_byte_str = self.context.insert_const_bytes(
+            self.builder.module,
+            bytes=kernel_module.kernel_bitcode,
+        )
+
+        kernel_name = self.context.insert_const_string(
+            self.builder.module, kernel_module.kernel_name
+        )
+
+        context_ref = sycl.dpctl_queue_get_context(self.builder, queue_ref)
+        device_ref = sycl.dpctl_queue_get_device(self.builder, queue_ref)
+
+        # build_or_get_kernel steals reference to context and device cause it
+        # needs to keep them alive for keys.
+        kernel_ref = self.dpexrt.build_or_get_kernel(
             self.builder,
-            utils.LLVMTypes.int32_t,
-            size=self.context.get_constant(types.uintp, num_kernel_args),
+            [
+                context_ref,
+                device_ref,
+                llvmir.Constant(
+                    llvmir.IntType(64), hash(kernel_module.kernel_bitcode)
+                ),
+                kernel_bc_byte_str,
+                llvmir.Constant(
+                    llvmir.IntType(64), len(kernel_module.kernel_bitcode)
+                ),
+                self.builder.load(create_null_ptr(self.builder, self.context)),
+                kernel_name,
+            ],
         )
 
-        return args_ty_list
+        self._cleanups.append(self._clean_kernel_ref)
+        self.set_kernel(kernel_ref)
 
-    def _create_sycl_range(self, idx_range):
-        """_summary_
+    def _clean_kernel_ref(self):
+        sycl.dpctl_kernel_delete(self.builder, self.arguments.sycl_kernel_ref)
+        self.arguments.sycl_kernel_ref = None
 
-        Args:
-            idx_range (_type_): _description_
-        """
-        intp_t = utils.get_llvm_type(context=self.context, type=types.intp)
-        intp_ptr_t = utils.get_llvm_ptr_type(intp_t)
-        num_dim = len(idx_range)
+    def set_queue(self, sycl_queue_ref: llvmir.Instruction):
+        """Sets queue to the argument list."""
+        self.arguments.sycl_queue_ref = sycl_queue_ref
 
-        # form the global range
-        global_range = cgutils.alloca_once(
+    def set_queue_from_arguments(
+        self,
+    ):
+        """Sets the sycl queue from the first USMNdArray argument provided
+        earlier."""
+        queue_ref = get_queue_from_llvm_values(
+            self.context,
             self.builder,
-            utils.get_llvm_type(context=self.context, type=types.uintp),
-            size=self.context.get_constant(types.uintp, num_dim),
-        )
-
-        for i in range(num_dim):
-            rext = idx_range[i]
-            if rext.type != utils.LLVMTypes.int64_t:
-                rext = self.builder.sext(rext, utils.LLVMTypes.int64_t)
-
-            # we reverse the global range to account for how sycl and opencl
-            # range differs
-            self.builder.store(
-                rext,
-                self.builder.gep(
-                    global_range,
-                    [self.context.get_constant(types.uintp, (num_dim - 1) - i)],
-                ),
+            self.cached_arguments.arg_ty_list,
+            self.cached_arguments.arg_list,
+        )
+
+        if queue_ref is None:
+            raise ValueError("There are no arguments that contain queue")
+
+        self.set_queue(queue_ref)
+
+    def set_range(
+        self,
+        global_range: list,
+        local_range: list = None,
+    ):
+        """Sets global and local range if provided to the argument list."""
+        self.arguments.global_range = self._create_sycl_range(global_range)
+        if local_range is not None and len(local_range) > 0:
+            self.arguments.local_range = self._create_sycl_range(local_range)
+        self.arguments.range_size = self.context.get_constant(
+            types.uintp, len(global_range)
+        )
+
+    def set_range_from_indexer(
+        self,
+        ty_indexer_arg: Union[RangeType, NdRangeType],
+        ll_index_arg: llvmir.BaseStructType,
+    ):
+        """Returns two lists of LLVM IR Values that hold the unboxed extents of
+        a Python Range or NdRange object.
+        """
+        ndim = ty_indexer_arg.ndim
+        global_range_extents = []
+        local_range_extents = []
+        indexer_datamodel = self.context.data_model_manager.lookup(
+            ty_indexer_arg
+        )
+
+        if isinstance(ty_indexer_arg, RangeType):
+            for dim_num in range(ndim):
+                dim_pos = indexer_datamodel.get_field_position(
+                    "dim" + str(dim_num)
+                )
+                global_range_extents.append(
+                    self.builder.extract_value(ll_index_arg, dim_pos)
+                )
+        elif isinstance(ty_indexer_arg, NdRangeType):
+            for dim_num in range(ndim):
+                gdim_pos = indexer_datamodel.get_field_position(
+                    "gdim" + str(dim_num)
+                )
+                global_range_extents.append(
+                    self.builder.extract_value(ll_index_arg, gdim_pos)
+                )
+                ldim_pos = indexer_datamodel.get_field_position(
+                    "ldim" + str(dim_num)
+                )
+                local_range_extents.append(
+                    self.builder.extract_value(ll_index_arg, ldim_pos)
+                )
+        else:
+            raise UnreachableError
+
+        self.set_range(global_range_extents, local_range_extents)
+
+    def set_arguments(
+        self,
+        ty_kernel_args: list[types.Type],
+        kernel_args: list[llvmir.Instruction],
+    ):
+        """Sets flattened kernel args, kernel arg types and number of those
+        arguments to the argument list."""
+        if config.DEBUG_KERNEL_LAUNCHER:
+            cgutils.printf(
+                self.builder,
+                "DPEX-DEBUG: Populating kernel args and arg type arrays.\n",
             )
 
-        return self.builder.bitcast(global_range, intp_ptr_t)
+        self.cached_arguments.arg_ty_list = ty_kernel_args
+        self.cached_arguments.arg_list = kernel_args
+
+        num_flattened_kernel_args = self._get_num_flattened_kernel_args(
+            kernel_argtys=ty_kernel_args,
+        )
+
+        # Create LLVM values for the kernel args list and kernel arg types list
+        args_list = self._allocate_array(
+            types.voidptr,
+            num_flattened_kernel_args,
+        )
+
+        args_ty_list = self._allocate_array(
+            types.int32,
+            num_flattened_kernel_args,
+        )
+
+        kernel_args_ptrs = []
+        for arg in kernel_args:
+            with self.builder.goto_entry_block():
+                ptr = self.builder.alloca(arg.type)
+            self.builder.store(arg, ptr)
+            kernel_args_ptrs.append(ptr)
+
+        # Populate the args_list and the args_ty_list LLVM arrays
+        self._populate_kernel_args_and_args_ty_arrays(
+            host_callargs_ptrs=kernel_args_ptrs,
+            host_kernel_argtys=ty_kernel_args,
+            kernel_args_list=args_list,
+            kernel_args_ty_list=args_ty_list,
+        )
+
+        self.arguments.arg_list = args_list
+        self.arguments.arg_ty_list = args_ty_list
+        self.arguments.total_kernel_args = self.context.get_constant(
+            types.uintp, num_flattened_kernel_args
+        )
 
-    def submit_sync_kernel(
+    def _extract_llvm_values_from_tuple(
         self,
-        sycl_queue_val,
-        total_kernel_args,
-        arg_list,
-        arg_ty_list,
-        global_range,
-        local_range=None,
+        ll_tuple: llvmir.Instruction,
+    ) -> list[llvmir.Instruction]:
+        """Extracts LLVM IR values from llvm tuple into python array."""
+
+        llvm_values = []
+        for pos in range(len(ll_tuple.type)):
+            llvm_values.append(self.builder.extract_value(ll_tuple, pos))
+
+        return llvm_values
+
+    def set_arguments_form_tuple(
+        self,
+        ty_kernel_args_tuple: UniTuple,
+        ll_kernel_args_tuple: llvmir.Instruction,
     ):
+        """Sets flattened kernel args, kernel arg types and number of those
+        arguments to the argument list based on the arguments stored in tuple.
         """
-        Submits the kernel to the specified queue, waits.
+        kernel_args = self._extract_llvm_values_from_tuple(ll_kernel_args_tuple)
+        self.set_arguments(ty_kernel_args_tuple, kernel_args)
+
+    def set_dependent_events(self, dep_events: list[llvmir.Instruction]):
+        """Sets dependent events to the argument list."""
+        ll_dep_events = self._create_ll_from_py_list(types.voidptr, dep_events)
+        self.arguments.dep_events = ll_dep_events
+        self.arguments.dep_events_len = self.context.get_constant(
+            types.uintp, len(dep_events)
+        )
+
+    def set_dependent_events_from_tuple(
+        self,
+        ty_dependent_events: UniTuple,
+        ll_dependent_events: llvmir.Instruction,
+    ):
+        """Set's dependent events from tuple represented by LLVM IR.
+
+        Args:
+            ll_dependent_events: tuple of numba's data models.
         """
-        gr = self._create_sycl_range(global_range)
-        args1 = [
-            self.builder.inttoptr(
-                self.context.get_constant(types.uintp, self.kernel_addr),
-                utils.get_llvm_type(context=self.context, type=types.voidptr),
-            ),
-            self.builder.load(sycl_queue_val),
-            arg_list,
-            arg_ty_list,
-            self.context.get_constant(types.uintp, total_kernel_args),
-            gr,
-        ]
-        args2 = [
-            self.context.get_constant(types.uintp, len(global_range)),
-            self.builder.bitcast(
-                utils.create_null_ptr(
-                    builder=self.builder, context=self.context
-                ),
-                utils.get_llvm_type(context=self.context, type=types.voidptr),
-            ),
-            self.context.get_constant(types.uintp, 0),
-        ]
-        args = []
-        if len(local_range) == 0:
-            args = args1 + args2
-            self.rtctx.submit_range(self.builder, *args)
+        if len(ty_dependent_events) == 0:
+            self.set_dependent_events([])
+            return
+
+        ty_event = ty_dependent_events[0]
+        dm_dependent_events = self._extract_llvm_values_from_tuple(
+            ll_dependent_events
+        )
+        dependent_events = []
+        for dm_dependent_event in dm_dependent_events:
+            event_struct_proxy = cgutils.create_struct_proxy(ty_event)(
+                self.context,
+                self.builder,
+                value=dm_dependent_event,
+            )
+            dependent_events.append(event_struct_proxy.event_ref)
+
+        self.set_dependent_events(dependent_events)
+
+    def submit(self) -> llvmir.Instruction:
+        """Submits kernel by calling sycl.dpctl_queue_submit_range or
+        sycl.dpctl_queue_submit_ndrange. Must be called after all arguments
+        set."""
+        args = self.arguments.to_list()
+
+        if self.arguments.local_range is None:
+            event_ref = sycl.dpctl_queue_submit_range(self.builder, *args)
         else:
-            lr = self._create_sycl_range(local_range)
-            args = args1 + [lr] + args2
-            self.rtctx.submit_ndrange(self.builder, *args)
+            event_ref = sycl.dpctl_queue_submit_ndrange(self.builder, *args)
+
+        self.cached_arguments.device_event_ref = event_ref
+
+        for cleanup in self._cleanups:
+            cleanup()
+
+        return event_ref
+
+    def _allocate_meminfo_array(
+        self,
+    ) -> tuple[int, list[llvmir.Instruction]]:
+        """Allocates an LLVM array value to store each memory info from all
+        kernel arguments. The array is the populated with the LLVM value for
+        every meminfo of the kernel arguments.
+        """
+        kernel_args = self.cached_arguments.arg_list
+        kernel_argtys = self.cached_arguments.arg_ty_list
+
+        meminfos = []
+        for arg_num, argtype in enumerate(kernel_argtys):
+            llvm_val = kernel_args[arg_num]
+
+            meminfos += [
+                meminfo
+                for ty, meminfo in self.context.nrt.get_meminfos(
+                    self.builder, argtype, llvm_val
+                )
+            ]
+
+        meminfo_list = self._create_ll_from_py_list(types.voidptr, meminfos)
+
+        return len(meminfos), meminfo_list
+
+    def acquire_meminfo_and_submit_release(
+        self,
+    ) -> llvmir.Instruction:
+        """Schedule sycl host task to release nrt meminfo of the arguments used
+        to run job. Use it to keep arguments alive during kernel execution."""
+        queue_ref = self.arguments.sycl_queue_ref
+        event_ref = self.cached_arguments.device_event_ref
+
+        total_meminfos, meminfo_list = self._allocate_meminfo_array()
+
+        event_ref_ptr = self.builder.alloca(event_ref.type)
+        self.builder.store(event_ref, event_ref_ptr)
+
+        status_ptr = cgutils.alloca_once(
+            self.builder, self.context.get_value_type(types.uint64)
+        )
+        host_eref = self.dpexrt.acquire_meminfo_and_schedule_release(
+            self.builder,
+            [
+                self.context.nrt.get_nrt_api(self.builder),
+                queue_ref,
+                meminfo_list,
+                self.context.get_constant(types.uintp, total_meminfos),
+                event_ref_ptr,
+                self.context.get_constant(types.uintp, 1),
+                status_ptr,
+            ],
+        )
+        return host_eref
+
+    def _get_num_flattened_kernel_args(
+        self,
+        kernel_argtys: tuple[types.Type, ...],
+    ) -> int:
+        """Returns number of flattened arguments of kernel data model based on
+        the numba types. Flattens usm arrays and complex values."""
+        num_flattened_kernel_args = 0
+        for arg_type in kernel_argtys:
+            if isinstance(arg_type, USMNdArray):
+                datamodel = self.kernel_dmm.lookup(arg_type)
+                num_flattened_kernel_args += datamodel.flattened_field_count
+            elif arg_type in [types.complex64, types.complex128]:
+                num_flattened_kernel_args += 2
+            else:
+                num_flattened_kernel_args += 1
+
+        return num_flattened_kernel_args
+
+    def _populate_kernel_args_and_args_ty_arrays(
+        self,
+        host_kernel_argtys,
+        host_callargs_ptrs,
+        kernel_args_list,
+        kernel_args_ty_list,
+    ):
+        kernel_arg_num = 0
+        for arg_num, argtype in enumerate(host_kernel_argtys):
+            host_llvm_val = host_callargs_ptrs[arg_num]
+            if isinstance(argtype, USMNdArray):
+                kernel_datamodel = self.kernel_dmm.lookup(argtype)
+                host_datamodel = self.context.data_model_manager.lookup(argtype)
+                self._build_array_arg(
+                    host_array_val=host_llvm_val,
+                    host_array_data_model=host_datamodel,
+                    kernel_array_data_model=kernel_datamodel,
+                    arg_list=kernel_args_list,
+                    args_ty_list=kernel_args_ty_list,
+                    arg_num=kernel_arg_num,
+                )
+                kernel_arg_num += kernel_datamodel.flattened_field_count
+            else:
+                if argtype == types.complex64:
+                    self._build_complex_arg(
+                        host_llvm_val,
+                        types.float32,
+                        kernel_args_list,
+                        kernel_args_ty_list,
+                        kernel_arg_num,
+                    )
+                    kernel_arg_num += 2
+                elif argtype == types.complex128:
+                    self._build_complex_arg(
+                        host_llvm_val,
+                        types.float64,
+                        kernel_args_list,
+                        kernel_args_ty_list,
+                        kernel_arg_num,
+                    )
+                    kernel_arg_num += 2
+                else:
+                    self._build_arg(
+                        host_llvm_val,
+                        argtype,
+                        kernel_args_list,
+                        kernel_args_ty_list,
+                        kernel_arg_num,
+                    )
+                    kernel_arg_num += 1
+
+
+def get_queue_from_llvm_values(
+    ctx: CPUContext,
+    builder: IRBuilder,
+    ty_kernel_args: list[types.Type],
+    ll_kernel_args: list[llvmir.Instruction],
+):
+    """
+    Get the sycl queue from the first USMNdArray argument. Prior passes
+    before lowering make sure that compute-follows-data is enforceable
+    for a specific call to a kernel. As such, at the stage of lowering
+    the queue from the first USMNdArray argument can be extracted.
+    """
+    for arg_num, argty in enumerate(ty_kernel_args):
+        if isinstance(argty, USMNdArray):
+            llvm_val = ll_kernel_args[arg_num]
+            datamodel = ctx.data_model_manager.lookup(argty)
+            sycl_queue_attr_pos = datamodel.get_field_position("sycl_queue")
+            queue_ref = builder.extract_value(llvm_val, sycl_queue_attr_pos)
+            break
+
+    return queue_ref
diff --git a/numba_dpex/core/utils/kernel_templates/__init__.py b/numba_dpex/core/utils/kernel_templates/__init__.py
index d826f682a1..2a7bd00b30 100644
--- a/numba_dpex/core/utils/kernel_templates/__init__.py
+++ b/numba_dpex/core/utils/kernel_templates/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/utils/kernel_templates/kernel_template_iface.py b/numba_dpex/core/utils/kernel_templates/kernel_template_iface.py
index 65cc84b6d4..453017dbbb 100644
--- a/numba_dpex/core/utils/kernel_templates/kernel_template_iface.py
+++ b/numba_dpex/core/utils/kernel_templates/kernel_template_iface.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/utils/kernel_templates/range_kernel_template.py b/numba_dpex/core/utils/kernel_templates/range_kernel_template.py
index 7ee5e0963b..026a707ba4 100644
--- a/numba_dpex/core/utils/kernel_templates/range_kernel_template.py
+++ b/numba_dpex/core/utils/kernel_templates/range_kernel_template.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/utils/kernel_templates/reduction_template.py b/numba_dpex/core/utils/kernel_templates/reduction_template.py
index 882570bce1..8752adf2f5 100644
--- a/numba_dpex/core/utils/kernel_templates/reduction_template.py
+++ b/numba_dpex/core/utils/kernel_templates/reduction_template.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/core/utils/suai_helper.py b/numba_dpex/core/utils/suai_helper.py
index df0a3d11b4..195b572cd7 100644
--- a/numba_dpex/core/utils/suai_helper.py
+++ b/numba_dpex/core/utils/suai_helper.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/debuginfo.py b/numba_dpex/debuginfo.py
index c0b8c45f2a..cef6f682b7 100644
--- a/numba_dpex/debuginfo.py
+++ b/numba_dpex/debuginfo.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/decorators.py b/numba_dpex/decorators.py
index 8f16f3b6d0..97e1ed8028 100644
--- a/numba_dpex/decorators.py
+++ b/numba_dpex/decorators.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -8,12 +8,14 @@
 from numba.core import decorators, sigutils
 from numba.core.target_extension import jit_registry, target_registry
 
+from numba_dpex.core import config
 from numba_dpex.core.kernel_interface.dispatcher import JitKernel
 from numba_dpex.core.kernel_interface.func import (
     compile_func,
     compile_func_template,
 )
-from numba_dpex.core.pipelines.dpjit_compiler import DpjitCompiler
+from numba_dpex.core.pipelines.dpjit_compiler import get_compiler
+from numba_dpex.core.targets.dpjit_target import DPEX_TARGET_NAME
 
 
 def kernel(
@@ -138,7 +140,7 @@ def _wrapped(pyfunc):
 
 
 def dpjit(*args, **kws):
-    if "nopython" in kws:
+    if "nopython" in kws and kws["nopython"] is not True:
         warnings.warn(
             "nopython is set for dpjit and is ignored", RuntimeWarning
         )
@@ -151,19 +153,19 @@ def dpjit(*args, **kws):
         warnings.warn(
             "pipeline class is set for dpjit and is ignored", RuntimeWarning
         )
-        del kws["forceobj"]
+        del kws["pipeline_class"]
+
+    use_mlir = kws.pop("use_mlir", bool(config.USE_MLIR))
+
     kws.update({"nopython": True})
     kws.update({"parallel": True})
-    kws.update({"pipeline_class": DpjitCompiler})
-
-    # FIXME: When trying to use dpex's target context, overloads do not work
-    # properly. We will turn on dpex target once the issue is fixed.
+    kws.update({"pipeline_class": get_compiler(use_mlir)})
 
-    # kws.update({"_target": "dpex"})
+    kws.update({"_target": DPEX_TARGET_NAME})
 
     return decorators.jit(*args, **kws)
 
 
 # add it to the decorator registry, this is so e.g. @overload can look up a
 # JIT function to do the compilation work.
-jit_registry[target_registry["dpex"]] = dpjit
+jit_registry[target_registry[DPEX_TARGET_NAME]] = dpjit
diff --git a/numba_dpex/dpctl_iface/__init__.py b/numba_dpex/dpctl_iface/__init__.py
index ef9b2784d4..31638b1013 100644
--- a/numba_dpex/dpctl_iface/__init__.py
+++ b/numba_dpex/dpctl_iface/__init__.py
@@ -1,30 +1,8 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 """
-The ``dpctl_iface`` module implements Numba's interface to the dpctl library
-that provides Python and C bindings to DPC++'s SYCL runtime API. The module
-includes:
-
-- LLVM IR builders for dpctl C API functions to be called directly from a Numba
-  generated LLVM module.
-- Functions to launch kernels on the dpctl "current queue".
-
+The ``dpctl_iface`` module implements Numba's interface to the libsyclinterface
+library that provides C bindings to DPC++'s SYCL runtime API.
 """
-import numba_dpex.dpctl_iface.dpctl_function_types as dpctl_fn_ty
-from numba_dpex.dpctl_iface.dpctl_capi_fn_builder import DpctlCAPIFnBuilder
-from numba_dpex.dpctl_iface.legacy_kernel_launch_ops import KernelLaunchOps
-
-__all__ = [
-    DpctlCAPIFnBuilder,
-    KernelLaunchOps,
-]
-
-get_current_queue = dpctl_fn_ty.dpctl_get_current_queue()
-malloc_shared = dpctl_fn_ty.dpctl_malloc_shared()
-queue_memcpy = dpctl_fn_ty.dpctl_queue_memcpy()
-free_with_queue = dpctl_fn_ty.dpctl_free_with_queue()
-event_wait = dpctl_fn_ty.dpctl_event_wait()
-event_delete = dpctl_fn_ty.dpctl_event_delete()
-queue_wait = dpctl_fn_ty.dpctl_queue_wait()
diff --git a/numba_dpex/dpctl_iface/_helpers.py b/numba_dpex/dpctl_iface/_helpers.py
index 604ba659bb..2814eb1aff 100644
--- a/numba_dpex/dpctl_iface/_helpers.py
+++ b/numba_dpex/dpctl_iface/_helpers.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/dpctl_iface/_intrinsic.py b/numba_dpex/dpctl_iface/_intrinsic.py
new file mode 100644
index 0000000000..d0eb2243da
--- /dev/null
+++ b/numba_dpex/dpctl_iface/_intrinsic.py
@@ -0,0 +1,80 @@
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpctl
+from llvmlite.ir import IRBuilder
+from numba import types
+from numba.core.datamodel import default_manager
+from numba.extending import intrinsic, overload, overload_method
+
+import numba_dpex.dpctl_iface.libsyclinterface_bindings as sycl
+from numba_dpex.core import types as dpex_types
+from numba_dpex.core.targets.dpjit_target import DPEX_TARGET_NAME
+from numba_dpex.dpctl_iface.wrappers import wrap_event_reference
+
+
+@intrinsic(target=DPEX_TARGET_NAME)
+def sycl_event_create(
+    ty_context,
+):
+    """A numba "intrinsic" function to inject dpctl.SyclEvent constructor code.
+
+    Args:
+        ty_context (numba.core.typing.context.Context): The typing context
+            for the codegen.
+
+    Returns:
+        tuple(numba.core.typing.templates.Signature, function): A tuple of
+            numba function signature type and a function object.
+    """
+    ty_event = dpex_types.DpctlSyclEvent()
+
+    sig = ty_event(types.void)
+
+    def codegen(context, builder: IRBuilder, sig, args: list):
+        event = sycl.dpctl_event_create(builder)
+        return wrap_event_reference(context, builder, event)
+
+    return sig, codegen
+
+
+@intrinsic(target=DPEX_TARGET_NAME)
+def sycl_event_wait(typingctx, ty_event: dpex_types.DpctlSyclEvent):
+    sig = types.void(dpex_types.DpctlSyclEvent())
+
+    # defines the custom code generation
+    def codegen(context, builder, signature, args):
+        sycl_event_dm = default_manager.lookup(ty_event)
+        event_ref = builder.extract_value(
+            args[0],
+            sycl_event_dm.get_field_position("event_ref"),
+        )
+
+        sycl.dpctl_event_wait(builder, event_ref)
+
+    return sig, codegen
+
+
+@overload(dpctl.SyclEvent, target=DPEX_TARGET_NAME)
+def ol_dpctl_sycl_event_create():
+    """Implementation of an overload to support dpctl.SyclEvent() inside
+    a dpjit function.
+    """
+    return lambda: sycl_event_create()
+
+
+@overload_method(dpex_types.DpctlSyclEvent, "wait", target=DPEX_TARGET_NAME)
+def ol_dpctl_sycl_event_wait(
+    event,
+):
+    """Implementation of an overload to support dpctl.SyclEvent.wait() inside
+    a dpjit function.
+    """
+    return lambda event: sycl_event_wait(event)
+
+
+# We don't want user to call sycl_event_wait(event), instead it must be called
+# with event.wait(). In that way we guarantee the argument type by the
+# @overload_method.
+__all__ = []
diff --git a/numba_dpex/dpctl_iface/dpctl_capi_fn_builder.py b/numba_dpex/dpctl_iface/dpctl_capi_fn_builder.py
deleted file mode 100644
index 4b1f9be368..0000000000
--- a/numba_dpex/dpctl_iface/dpctl_capi_fn_builder.py
+++ /dev/null
@@ -1,287 +0,0 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""
-This module provides a set of wrapper functions to insert dpctl C API function
-declarations into an LLVM module.
-"""
-
-from llvmlite import ir as llvmir
-from numba.core import cgutils, types
-
-import numba_dpex.utils as utils
-
-
-class DpctlCAPIFnBuilder:
-    """
-    Defines a set of static functions to add declarations for dpctl C API
-    library function into an LLVM module.
-    """
-
-    @staticmethod
-    def _build_dpctl_function(builder, return_ty, arg_list, func_name):
-        """
-        _build_dpctl_function(builder, return_ty, arg_list, func_name)
-        Inserts an LLVM function of the specified signature and name into an
-        LLVM module.
-
-        Args:
-            return_ty: An LLVM Value corresponding to the return type of the
-                       function.
-            arg_list: A list of LLVM Value objects corresponsing to the
-                      type of the arguments for the function.
-            func_name: The name of the function passed as a string.
-
-        Return: A Python object wrapping an LLVM Function.
-
-        """
-        func_ty = llvmir.FunctionType(return_ty, arg_list)
-        fn = cgutils.get_or_insert_function(builder.module, func_ty, func_name)
-        return fn
-
-    @staticmethod
-    def get_dpctl_queuemgr_get_current_queue(builder, context):
-        """
-        get_dpctl_queuemgr_get_current_queue(builder, context)
-        Inserts an LLVM Function for ``DPCTLQueueMgr_GetCurrentQueue``.
-
-        The ``DPCTLQueueMgr_GetCurrentQueue`` function returns the current
-        top-of-the-stack SYCL queue that is stored in dpclt's queue manager.
-
-        Args:
-            builder: The LLVM IR builder to be used for code generation.
-            context: The LLVM IR builder context.
-
-        Return: A Python object wrapping an LLVM Function for
-                ``DPCTLQueueMgr_GetCurrentQueue``.
-        """
-        return DpctlCAPIFnBuilder._build_dpctl_function(
-            builder,
-            return_ty=utils.get_llvm_type(context=context, type=types.voidptr),
-            arg_list=[],
-            func_name="DPCTLQueueMgr_GetCurrentQueue",
-        )
-
-    @staticmethod
-    def get_dpctl_queue_delete(builder, context):
-        """
-        get_dpctl_queue_delete(builder, context)
-        Inserts an LLVM Function for the ``DPCTLQueue_Delete``.
-
-        The ``DPCTLQueue_Delete`` deletes a DPCTLSyclQueueRef opaque pointer.
-
-        Args:
-            builder: The LLVM IR builder to be used for code generation.
-            context: The LLVM IR builder context.
-
-        Return: A Python object wrapping an LLVM Function for
-                ``DPCTLQueue_Delete``.
-        """
-        return DpctlCAPIFnBuilder._build_dpctl_function(
-            builder,
-            return_ty=utils.LLVMTypes.void_t,
-            arg_list=[utils.get_llvm_type(context=context, type=types.voidptr)],
-            func_name="DPCTLQueue_Delete",
-        )
-
-    @staticmethod
-    def get_dpctl_queue_memcpy(builder, context):
-        """
-        get_dpctl_queue_memcpy(builder, context)
-        Inserts an LLVM Function for the ``DPCTLQueue_Memcpy``.
-
-        The ``DPCTLQueue_Memcpy`` function is a wrapper over ``sycl::queue``
-        class' ``event memcpy(void* dest, const void* src, size_t numBytes)``
-        function. Currently, the DPCTLQueue_Memcpy does not return an event
-        reference, but in future will return an opaque pointer to a
-        ``sycl::event``. All the opaque pointers arguments to the
-        ``DPCTLQueue_Memcpy`` are passed as void pointers.
-
-        Args:
-            builder: The LLVM IR builder to be used for code generation.
-            context: The LLVM IR builder context.
-
-        Return: A Python object wrapping an LLVM Function for
-                ``DPCTLQueue_Memcpy``.
-        """
-        void_ptr_t = utils.get_llvm_type(context=context, type=types.voidptr)
-        return DpctlCAPIFnBuilder._build_dpctl_function(
-            builder,
-            return_ty=void_ptr_t,
-            # return_ty=utils.LLVMTypes.void_t,
-            arg_list=[
-                void_ptr_t,
-                void_ptr_t,
-                void_ptr_t,
-                utils.get_llvm_type(context=context, type=types.intp),
-            ],
-            func_name="DPCTLQueue_Memcpy",
-        )
-
-    @staticmethod
-    def get_dpctl_queue_wait(builder, context):
-        """
-        get_dpctl_queue_wait(builder, context)
-        Inserts an LLVM Function for ``DPCTLQueue_Wait``.
-
-        The ``DPCTLQueue_Wait`` is a wrapper over ``sycl::queue`` class'
-        ``wait()`` function.
-
-        Args:
-            builder: The LLVM IR builder to be used for code generation.
-            context: The LLVM IR builder context.
-
-        Returns: A Python object wrapping an LLVM Function for
-                ``DPCTLQueue_Wait``.
-        """
-        void_ptr_t = utils.get_llvm_type(context=context, type=types.voidptr)
-        return DpctlCAPIFnBuilder._build_dpctl_function(
-            builder,
-            return_ty=utils.LLVMTypes.void_t,
-            arg_list=[void_ptr_t],
-            func_name="DPCTLQueue_Wait",
-        )
-
-    @staticmethod
-    def get_dpctl_queue_submit_range(builder, context):
-        """
-        get_dpctl_queue_submit_range(builder, context)
-        Inserts an LLVM Function for ``DPCTLQueue_SubmitRange``.
-
-        The ``DPCTLQueue_SubmitRange`` function is a wrapper over the
-        ``sycl::queue`` class' ``event parallel_for(range<dimensions>
-        numWorkItems, Rest&&... rest)`` function. All the opaque pointers
-        arguments to the ``DPCTLQueue_SubmitRange`` function are passed as void
-        pointers.
-
-        Note: The ``DPCTLQueue_SubmitRange`` calls returns an opaque pointer to
-        a ``sycl::event`` that needs to be destroyed properly.
-
-        Args:
-            builder: The LLVM IR builder to be used for code generation.
-            context: The LLVM IR builder context.
-
-        Return: A Python object wrapping an LLVM Function for
-                ``DPCTLQueue_SubmitRange``.
-        """
-        intp_t = utils.get_llvm_type(context=context, type=types.intp)
-        void_ptr_t = utils.get_llvm_type(context=context, type=types.voidptr)
-        void_ptr_ptr_t = utils.get_llvm_ptr_type(void_ptr_t)
-        intp_ptr_t = utils.get_llvm_ptr_type(intp_t)
-
-        return DpctlCAPIFnBuilder._build_dpctl_function(
-            builder,
-            return_ty=void_ptr_t,
-            arg_list=[
-                void_ptr_t,
-                void_ptr_t,
-                void_ptr_ptr_t,
-                utils.LLVMTypes.int32_ptr_t,
-                utils.get_llvm_type(context=context, type=types.intp),
-                intp_ptr_t,
-                utils.get_llvm_type(context=context, type=types.intp),
-                void_ptr_t,
-                utils.get_llvm_type(context=context, type=types.intp),
-            ],
-            func_name="DPCTLQueue_SubmitRange",
-        )
-
-    @staticmethod
-    def get_dpctl_malloc_shared(builder, context):
-        """
-        get_dpctl_malloc_shared(builder, context)
-        Inserts an LLVM Function for ``DPCTLmalloc_shared``.
-
-        ``DPCTLmalloc_shared`` is a wrapper over the
-        ``sycl::malloc_shared`` function to allocate USM shared memory.
-
-        Args:
-            builder: The LLVM IR builder to be used for code generation.
-            context: The LLVM IR builder context.
-
-        Return: A Python object wrapping an LLVM Function for
-                ``DPCTLmalloc_shared``.
-
-        """
-        void_ptr_t = utils.get_llvm_type(context=context, type=types.voidptr)
-        int_ptr_t = utils.get_llvm_type(context=context, type=types.intp)
-        return DpctlCAPIFnBuilder._build_dpctl_function(
-            builder,
-            return_ty=void_ptr_t,
-            arg_list=[int_ptr_t, void_ptr_t],
-            func_name="DPCTLmalloc_shared",
-        )
-
-    @staticmethod
-    def get_dpctl_free_with_queue(builder, context):
-        """
-        get_dpctl_free_with_queue(builder, context)
-        Inserts an LLVM Function for ``DPCTLfree_with_queue``.
-
-        The ``DPCTLfree_with_queue`` function is a wrapper over
-        ``sycl::free(void*, queue)`` function. All the opaque pointers
-        arguments to the ``DPCTLfree_with_queue`` are passed as void pointers.
-
-        Args:
-            builder: The LLVM IR builder to be used for code generation.
-            context: The LLVM IR builder context.
-
-        Return: A Python object wrapping an LLVM Function for
-                ``DPCTLfree_with_queue``.
-        """
-        void_ptr_t = utils.get_llvm_type(context=context, type=types.voidptr)
-        return DpctlCAPIFnBuilder._build_dpctl_function(
-            builder,
-            return_ty=utils.LLVMTypes.void_t,
-            arg_list=[void_ptr_t, void_ptr_t],
-            func_name="DPCTLfree_with_queue",
-        )
-
-    @staticmethod
-    def get_dpctl_event_wait(builder, context):
-        """
-        get_dpctl_event_wait(builder, context)
-        Inserts an LLVM Function for ``DPCTLEvent_Wait``.
-
-        The ``DPCTLEvent_Wait`` function is a wrapper over the
-        ``sycl::event`` class' ``wait()`` function.
-
-        Args:
-            builder: The LLVM IR builder to be used for code generation.
-            context: The LLVM IR builder context.
-
-        Return: A Python object wrapping an LLVM Function for
-                ``DPCTLEvent_Wait``.
-        """
-        void_ptr_t = utils.get_llvm_type(context=context, type=types.voidptr)
-        return DpctlCAPIFnBuilder._build_dpctl_function(
-            builder,
-            return_ty=utils.LLVMTypes.void_t,
-            arg_list=[void_ptr_t],
-            func_name="DPCTLEvent_Wait",
-        )
-
-    @staticmethod
-    def get_dpctl_event_delete(builder, context):
-        """
-        get_dpctl_event_delete(builder, context)
-        Inserts an LLVM Function for ``DPCTLEvent_Delete``.
-
-        The ``DPCTLEvent_Delete`` function deletes a ``DPCTLSyclEventRef``
-        opaque pointer.
-
-        Args:
-            builder: The LLVM IR builder to be used for code generation.
-            context: The LLVM IR builder context.
-
-        Returns: A Python object wrapping an LLVM Function for
-                 ``DPCTLEvent_Delete``.
-        """
-        void_ptr_t = utils.get_llvm_type(context=context, type=types.voidptr)
-        return DpctlCAPIFnBuilder._build_dpctl_function(
-            builder,
-            return_ty=utils.LLVMTypes.void_t,
-            arg_list=[void_ptr_t],
-            func_name="DPCTLEvent_Delete",
-        )
diff --git a/numba_dpex/dpctl_iface/dpctl_function_types.py b/numba_dpex/dpctl_iface/dpctl_function_types.py
deleted file mode 100644
index 5eeb5ad705..0000000000
--- a/numba_dpex/dpctl_iface/dpctl_function_types.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
-#
-# SPDX-License-Identifier: Apache-2.0
-
-from numba import types
-from numba.core.typing import signature
-
-
-def dpctl_get_current_queue():
-    ret_type = types.voidptr
-    sig = signature(ret_type)
-    return types.ExternalFunction("DPCTLQueueMgr_GetCurrentQueue", sig)
-
-
-def dpctl_malloc_shared():
-    ret_type = types.voidptr
-    sig = signature(ret_type, types.int64, types.voidptr)
-    return types.ExternalFunction("DPCTLmalloc_shared", sig)
-
-
-def dpctl_queue_memcpy():
-    ret_type = types.voidptr
-    args = types.voidptr, types.voidptr, types.voidptr, types.int64
-    sig = signature(ret_type, *args)
-    return types.ExternalFunction("DPCTLQueue_Memcpy", sig)
-
-
-def dpctl_event_wait():
-    ret_type = types.voidptr
-    sig = signature(ret_type, types.voidptr)
-    return types.ExternalFunction("DPCTLEvent_Wait", sig)
-
-
-def dpctl_event_delete():
-    ret_type = types.void
-    sig = signature(ret_type, types.voidptr)
-    return types.ExternalFunction("DPCTLEvent_Delete", sig)
-
-
-def dpctl_free_with_queue():
-    ret_type = types.void
-    sig = signature(ret_type, types.voidptr, types.voidptr)
-    return types.ExternalFunction("DPCTLfree_with_queue", sig)
-
-
-def dpctl_queue_wait():
-    ret_type = types.void
-    sig = signature(ret_type, types.voidptr)
-    return types.ExternalFunction("DPCTLQueue_Wait", sig)
diff --git a/numba_dpex/dpctl_iface/legacy_kernel_launch_ops.py b/numba_dpex/dpctl_iface/legacy_kernel_launch_ops.py
deleted file mode 100644
index 63db1141a7..0000000000
--- a/numba_dpex/dpctl_iface/legacy_kernel_launch_ops.py
+++ /dev/null
@@ -1,514 +0,0 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
-#
-# SPDX-License-Identifier: Apache-2.0
-
-from numba.core import cgutils, types
-from numba.core.ir_utils import legalize_names
-
-from numba_dpex import utils
-from numba_dpex.dpctl_iface import DpctlCAPIFnBuilder
-from numba_dpex.dpctl_iface._helpers import numba_type_to_dpctl_typenum
-
-
-class KernelLaunchOps:
-    """
-    KernelLaunchOps(lowerer, cres, num_inputs)
-    Defines a set of functions to launch a SYCL kernel on the "current queue"
-    as defined in the dpctl queue manager.
-    """
-
-    def _form_kernel_arg_and_arg_ty(self, val, ty):
-        kernel_arg_dst = self.builder.gep(
-            self.kernel_arg_array,
-            [self.context.get_constant(types.int32, self.cur_arg)],
-        )
-        kernel_arg_ty_dst = self.builder.gep(
-            self.kernel_arg_ty_array,
-            [self.context.get_constant(types.int32, self.cur_arg)],
-        )
-        self.cur_arg += 1
-        self.builder.store(val, kernel_arg_dst)
-        self.builder.store(ty, kernel_arg_ty_dst)
-
-    def __init__(self, lowerer, kernel, num_inputs):
-        """Create an instance of KernelLaunchOps.
-
-        Args:
-            lowerer: The Numba Lowerer that will be used to generate the code.
-            kernel: The SYCL kernel for which we are generating the code.
-            num_inputs: The number of arguments to the kernels.
-        """
-        self.lowerer = lowerer
-        self.context = self.lowerer.context
-        self.builder = self.lowerer.builder
-        self.kernel = kernel
-        self.kernel_addr = self.kernel.addressof_ref()
-        self.total_kernel_args = 0
-        self.cur_arg = 0
-        self.num_inputs = num_inputs
-        # list of buffer that needs to comeback to host
-        self.write_buffs = []
-        # list of buffer that does not need to comeback to host
-        self.read_only_buffs = []
-
-    def get_current_queue(self):
-        """Allocates memory on the stack to store the current queue from dpctl.
-
-        A SYCL queue is needed to allocate USM memory and submit a kernel. This
-        function gets the queue returned by ``DPCTLQueueMgr_GetCurrentQueue``
-        function and stores it on the stack. The queue should be freed properly
-        after returning from the kernel.
-
-        Return: A LLVM Value storing the pointer to the SYCL queue returned
-                by ``DPCTLQueueMgr_GetCurrentQueue``.
-
-        """
-        sycl_queue_val = cgutils.alloca_once(
-            self.builder,
-            utils.get_llvm_type(context=self.context, type=types.voidptr),
-        )
-        fn = DpctlCAPIFnBuilder.get_dpctl_queuemgr_get_current_queue(
-            builder=self.builder, context=self.context
-        )
-        self.builder.store(self.builder.call(fn, []), sycl_queue_val)
-        return sycl_queue_val
-
-    def free_queue(self, sycl_queue_val):
-        """
-        Frees the ``DPCTLSyclQueueRef`` pointer that was used to launch the
-        kernels.
-
-        Args:
-            sycl_queue_val: The SYCL queue pointer to be freed.
-        """
-        fn = DpctlCAPIFnBuilder.get_dpctl_queue_delete(
-            builder=self.builder, context=self.context
-        )
-        self.builder.call(fn, [self.builder.load(sycl_queue_val)])
-
-    def allocate_kernel_arg_array(self, num_kernel_args):
-        """Allocates an array to store the LLVM Value for every kernel argument.
-
-        Args:
-            num_kernel_args : The number of kernel arguments for the kernel.
-
-        """
-
-        self.total_kernel_args = num_kernel_args
-
-        # we need a kernel arg array to enqueue
-        self.kernel_arg_array = cgutils.alloca_once(
-            self.builder,
-            utils.get_llvm_type(context=self.context, type=types.voidptr),
-            size=self.context.get_constant(types.uintp, num_kernel_args),
-            name="kernel_arg_array",
-        )
-
-        self.kernel_arg_ty_array = cgutils.alloca_once(
-            self.builder,
-            utils.LLVMTypes.int32_t,
-            size=self.context.get_constant(types.uintp, num_kernel_args),
-            name="kernel_arg_ty_array",
-        )
-
-    def process_kernel_arg(
-        self, var, llvm_arg, arg_type, index, modified_arrays, sycl_queue_val
-    ):
-        """
-        process_kernel_arg(var, llvm_arg, arg_type, index, modified_arrays,
-        sycl_queue_val)
-        Creates an LLVM Value for each kernel argument.
-
-        Args:
-            var : A kernel argument represented as a Numba type.
-            llvm_arg : Only used for array arguments and points to the LLVM
-            value previously allocated to store the array arg.
-            arg_type : The Numba type for the argument.
-            index : The poisition of the argument in the list of arguments.
-            modified_arrays : The list of array arguments that are written to
-            inside the kernel. The list is used to check if the argument is
-            read-only or not.
-
-        Raises:
-            NotImplementedError: If an unsupported type of kernel argument is
-            encountered.
-
-        """
-        if isinstance(arg_type, types.npytypes.Array):
-            if llvm_arg is None:
-                raise NotImplementedError(arg_type, var)
-
-            storage = cgutils.alloca_once(self.builder, utils.LLVMTypes.int64_t)
-            self.builder.store(
-                self.context.get_constant(types.int64, 0), storage
-            )
-            ty = numba_type_to_dpctl_typenum(
-                context=self.context, ty=types.int64
-            )
-            self._form_kernel_arg_and_arg_ty(
-                self.builder.bitcast(
-                    storage,
-                    utils.get_llvm_type(
-                        context=self.context, type=types.voidptr
-                    ),
-                ),
-                ty,
-            )
-
-            storage = cgutils.alloca_once(self.builder, utils.LLVMTypes.int64_t)
-            self.builder.store(
-                self.context.get_constant(types.int64, 0), storage
-            )
-            ty = numba_type_to_dpctl_typenum(
-                context=self.context, ty=types.int64
-            )
-            self._form_kernel_arg_and_arg_ty(
-                self.builder.bitcast(
-                    storage,
-                    utils.get_llvm_type(
-                        context=self.context, type=types.voidptr
-                    ),
-                ),
-                ty,
-            )
-
-            # Handle array size
-            array_size_member = self.builder.gep(
-                llvm_arg,
-                [
-                    self.context.get_constant(types.int32, 0),
-                    self.context.get_constant(types.int32, 2),
-                ],
-            )
-
-            ty = numba_type_to_dpctl_typenum(
-                context=self.context, ty=types.int64
-            )
-            self._form_kernel_arg_and_arg_ty(
-                self.builder.bitcast(
-                    array_size_member,
-                    utils.get_llvm_type(
-                        context=self.context, type=types.voidptr
-                    ),
-                ),
-                ty,
-            )
-
-            # Handle itemsize
-            item_size_member = self.builder.gep(
-                llvm_arg,
-                [
-                    self.context.get_constant(types.int32, 0),
-                    self.context.get_constant(types.int32, 3),
-                ],
-            )
-
-            ty = numba_type_to_dpctl_typenum(
-                context=self.context, ty=types.int64
-            )
-            self._form_kernel_arg_and_arg_ty(
-                self.builder.bitcast(
-                    item_size_member,
-                    utils.get_llvm_type(
-                        context=self.context, type=types.voidptr
-                    ),
-                ),
-                ty,
-            )
-
-            # Calculate total buffer size
-            total_size = cgutils.alloca_once(
-                self.builder,
-                utils.get_llvm_type(context=self.context, type=types.intp),
-                size=utils.get_one(context=self.context),
-                name="total_size" + str(self.cur_arg),
-            )
-            self.builder.store(
-                self.builder.sext(
-                    self.builder.mul(
-                        self.builder.load(array_size_member),
-                        self.builder.load(item_size_member),
-                    ),
-                    utils.get_llvm_type(context=self.context, type=types.intp),
-                ),
-                total_size,
-            )
-
-            # Handle data
-            data_member = self.builder.gep(
-                llvm_arg,
-                [
-                    self.context.get_constant(types.int32, 0),
-                    self.context.get_constant(types.int32, 4),
-                ],
-            )
-
-            # names are replaced using legalize names, we have to do the same
-            # here for them to match.
-            legal_names = legalize_names([var])
-            ty = numba_type_to_dpctl_typenum(
-                context=self.context, ty=types.voidptr
-            )
-
-            malloc_fn = DpctlCAPIFnBuilder.get_dpctl_malloc_shared(
-                builder=self.builder, context=self.context
-            )
-            memcpy_fn = DpctlCAPIFnBuilder.get_dpctl_queue_memcpy(
-                builder=self.builder, context=self.context
-            )
-            event_del_fn = DpctlCAPIFnBuilder.get_dpctl_event_delete(
-                builder=self.builder, context=self.context
-            )
-            event_wait_fn = DpctlCAPIFnBuilder.get_dpctl_event_wait(
-                builder=self.builder, context=self.context
-            )
-
-            # Not known to be USM so we need to copy to USM.
-            buffer_name = "buffer_ptr" + str(self.cur_arg)
-            # Create void * to hold new USM buffer.
-            buffer_ptr = cgutils.alloca_once(
-                self.builder,
-                utils.get_llvm_type(context=self.context, type=types.voidptr),
-                name=buffer_name,
-            )
-            # Setup the args to the USM allocator, size and SYCL queue.
-            args = [
-                self.builder.load(total_size),
-                self.builder.load(sycl_queue_val),
-            ]
-            # Call USM shared allocator and store in buffer_ptr.
-            self.builder.store(self.builder.call(malloc_fn, args), buffer_ptr)
-
-            if legal_names[var] in modified_arrays:
-                self.write_buffs.append((buffer_ptr, total_size, data_member))
-            else:
-                self.read_only_buffs.append(
-                    (buffer_ptr, total_size, data_member)
-                )
-
-            # We really need to detect when an array needs to be copied over
-            if index < self.num_inputs:
-                args = [
-                    self.builder.load(sycl_queue_val),
-                    self.builder.load(buffer_ptr),
-                    self.builder.bitcast(
-                        self.builder.load(data_member),
-                        utils.get_llvm_type(
-                            context=self.context, type=types.voidptr
-                        ),
-                    ),
-                    self.builder.load(total_size),
-                ]
-                event_ref = self.builder.call(memcpy_fn, args)
-                self.builder.call(event_wait_fn, [event_ref])
-                self.builder.call(event_del_fn, [event_ref])
-
-            self._form_kernel_arg_and_arg_ty(self.builder.load(buffer_ptr), ty)
-
-            # Handle shape
-            shape_member = self.builder.gep(
-                llvm_arg,
-                [
-                    self.context.get_constant(types.int32, 0),
-                    self.context.get_constant(types.int32, 5),
-                ],
-            )
-
-            for this_dim in range(arg_type.ndim):
-                shape_entry = self.builder.gep(
-                    shape_member,
-                    [
-                        self.context.get_constant(types.int32, 0),
-                        self.context.get_constant(types.int32, this_dim),
-                    ],
-                )
-                ty = numba_type_to_dpctl_typenum(
-                    context=self.context, ty=types.int64
-                )
-                self._form_kernel_arg_and_arg_ty(
-                    self.builder.bitcast(
-                        shape_entry,
-                        utils.get_llvm_type(
-                            context=self.context, type=types.voidptr
-                        ),
-                    ),
-                    ty,
-                )
-
-            # Handle strides
-            stride_member = self.builder.gep(
-                llvm_arg,
-                [
-                    self.context.get_constant(types.int32, 0),
-                    self.context.get_constant(types.int32, 6),
-                ],
-            )
-
-            for this_stride in range(arg_type.ndim):
-                stride_entry = self.builder.gep(
-                    stride_member,
-                    [
-                        self.context.get_constant(types.int32, 0),
-                        self.context.get_constant(types.int32, this_stride),
-                    ],
-                )
-
-                ty = numba_type_to_dpctl_typenum(
-                    context=self.context, ty=types.int64
-                )
-                self._form_kernel_arg_and_arg_ty(
-                    self.builder.bitcast(
-                        stride_entry,
-                        utils.get_llvm_type(
-                            context=self.context, type=types.voidptr
-                        ),
-                    ),
-                    ty,
-                )
-
-        else:
-            ty = numba_type_to_dpctl_typenum(context=self.context, ty=arg_type)
-            self._form_kernel_arg_and_arg_ty(
-                self.builder.bitcast(
-                    llvm_arg,
-                    utils.get_llvm_type(
-                        context=self.context, type=types.voidptr
-                    ),
-                ),
-                ty,
-            )
-
-    def enqueue_kernel_and_copy_back(self, dim_bounds, sycl_queue_val):
-        """
-        enqueue_kernel_and_copy_back(dim_bounds, sycl_queue_val)
-        Submits the kernel to the specified queue, waits and then copies
-        back any results to the host.
-
-        Args:
-            dim_bounds : An array of three tuple representing the starting
-                         offset, end offset and the stride (step) for each
-                         dimension of the input arrays. Every array in a parfor
-                         is of the same dimensionality and shape, thus ensuring
-                         the bounds are the same.
-            sycl_queue_val : The SYCL queue on which the kernel is
-                             submitted.
-        """
-        submit_fn = DpctlCAPIFnBuilder.get_dpctl_queue_submit_range(
-            builder=self.builder, context=self.context
-        )
-        queue_wait_fn = DpctlCAPIFnBuilder.get_dpctl_queue_wait(
-            builder=self.builder, context=self.context
-        )
-        event_del_fn = DpctlCAPIFnBuilder.get_dpctl_event_delete(
-            builder=self.builder, context=self.context
-        )
-        memcpy_fn = DpctlCAPIFnBuilder.get_dpctl_queue_memcpy(
-            builder=self.builder, context=self.context
-        )
-        free_fn = DpctlCAPIFnBuilder.get_dpctl_free_with_queue(
-            builder=self.builder, context=self.context
-        )
-        event_wait_fn = DpctlCAPIFnBuilder.get_dpctl_event_wait(
-            builder=self.builder, context=self.context
-        )
-
-        # the assumption is loop_ranges will always be less than or equal to 3
-        # dimensions
-        num_dim = len(dim_bounds) if len(dim_bounds) < 4 else 3
-
-        # form the global range
-        global_range = cgutils.alloca_once(
-            self.builder,
-            utils.get_llvm_type(context=self.context, type=types.uintp),
-            size=self.context.get_constant(types.uintp, num_dim),
-            name="global_range",
-        )
-
-        intp_t = utils.get_llvm_type(context=self.context, type=types.intp)
-        intp_ptr_t = utils.get_llvm_ptr_type(intp_t)
-
-        for i in range(num_dim):
-            start, stop, step = dim_bounds[i]
-            if stop.type != utils.LLVMTypes.int64_t:
-                stop = self.builder.sext(stop, utils.LLVMTypes.int64_t)
-
-            # we reverse the global range to account for how sycl and opencl
-            # range differs
-            self.builder.store(
-                stop,
-                self.builder.gep(
-                    global_range,
-                    [self.context.get_constant(types.uintp, (num_dim - 1) - i)],
-                ),
-            )
-
-        args = [
-            self.builder.inttoptr(
-                self.context.get_constant(types.uintp, self.kernel_addr),
-                utils.get_llvm_type(context=self.context, type=types.voidptr),
-            ),
-            self.builder.load(sycl_queue_val),
-            self.kernel_arg_array,
-            self.kernel_arg_ty_array,
-            self.context.get_constant(types.uintp, self.total_kernel_args),
-            self.builder.bitcast(global_range, intp_ptr_t),
-            self.context.get_constant(types.uintp, num_dim),
-            self.builder.bitcast(
-                utils.create_null_ptr(
-                    builder=self.builder, context=self.context
-                ),
-                utils.get_llvm_type(context=self.context, type=types.voidptr),
-            ),
-            self.context.get_constant(types.uintp, 0),
-        ]
-
-        # Submit the kernel
-        event_ref = self.builder.call(submit_fn, args)
-
-        # Add a wait on the queue
-        self.builder.call(queue_wait_fn, [self.builder.load(sycl_queue_val)])
-
-        # Note that the dpctl_queue_wait call waits on the event and then
-        # decrements the ref count of the sycl::event C++ object. However, the
-        # event object returned by the get_dpctl_queue_submit_range call still
-        # needs to be explicitly deleted to free up the event object properly.
-        self.builder.call(event_del_fn, [event_ref])
-
-        # read buffers back to host
-        for write_buff in self.write_buffs:
-            buffer_ptr, total_size, data_member = write_buff
-            args = [
-                self.builder.load(sycl_queue_val),
-                self.builder.bitcast(
-                    self.builder.load(data_member),
-                    utils.get_llvm_type(
-                        context=self.context, type=types.voidptr
-                    ),
-                ),
-                self.builder.load(buffer_ptr),
-                self.builder.load(total_size),
-            ]
-            # FIXME: In future, when the DctlQueue_Memcpy is made non-blocking
-            # the returned event should be explicitly freed by calling
-            # get_dpctl_event_delete.
-            event_ref = self.builder.call(memcpy_fn, args)
-            self.builder.call(event_wait_fn, [event_ref])
-            self.builder.call(event_del_fn, [event_ref])
-
-            self.builder.call(
-                free_fn,
-                [
-                    self.builder.load(buffer_ptr),
-                    self.builder.load(sycl_queue_val),
-                ],
-            )
-
-        for read_buff in self.read_only_buffs:
-            buffer_ptr, total_size, data_member = read_buff
-            self.builder.call(
-                free_fn,
-                [
-                    self.builder.load(buffer_ptr),
-                    self.builder.load(sycl_queue_val),
-                ],
-            )
diff --git a/numba_dpex/dpctl_iface/libsyclinterface_bindings.py b/numba_dpex/dpctl_iface/libsyclinterface_bindings.py
new file mode 100644
index 0000000000..83a73303b1
--- /dev/null
+++ b/numba_dpex/dpctl_iface/libsyclinterface_bindings.py
@@ -0,0 +1,338 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""llvmlite function call generators for dpctl's libsyclinterface library"""
+
+from llvmlite import ir as llvmir
+from numba.core import cgutils
+
+
+def _build_dpctl_function(llvm_module, return_ty, arg_list, func_name):
+    """
+    _build_dpctl_function(llvm_module, return_ty, arg_list, func_name)
+    Inserts an LLVM function of the specified signature and name into an
+    LLVM module.
+
+    Args:
+        llvm_module: The LLVM IR Module into which the function will be inserted
+        return_ty: An LLVM Value corresponding to the return type of the
+                   function.
+        arg_list: A list of LLVM Value objects corresponding to the
+                  type of the arguments for the function.
+        func_name: The name of the function passed as a string.
+
+    Return: A Python object wrapping an LLVM Function.
+
+    """
+    func_ty = llvmir.FunctionType(return_ty, arg_list)
+    fn = cgutils.get_or_insert_function(llvm_module, func_ty, func_name)
+    return fn
+
+
+# --------------------- DpctlSyclQueue functions ------------------------------#
+
+
+def dpctl_queue_copy(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLQueue_Copy to create a copy of a
+    DpctlSyclQueueRef pointer passed in to the function.
+    """
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=cgutils.voidptr_t,
+        arg_list=[cgutils.voidptr_t],
+        func_name="DPCTLQueue_Copy",
+    )
+
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+def dpctl_queue_memcpy(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLQueue_Memcpy."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=cgutils.voidptr_t,
+        arg_list=[
+            cgutils.voidptr_t,
+            cgutils.voidptr_t,
+            cgutils.voidptr_t,
+            cgutils.intp_t,
+        ],
+        func_name="DPCTLQueue_Memcpy",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+def dpctl_event_create(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLEvent_Create."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=cgutils.voidptr_t,
+        arg_list=[],
+        func_name="DPCTLEvent_Create",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+def dpctl_queue_delete(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLQueue_Delete."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=llvmir.types.VoidType(),
+        arg_list=[cgutils.voidptr_t],
+        func_name="DPCTLQueue_Delete",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+def dpctl_queue_get_context(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLQueue_GetContext."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=cgutils.voidptr_t,
+        arg_list=[cgutils.voidptr_t],
+        func_name="DPCTLQueue_GetContext",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+def dpctl_queue_get_device(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLQueue_GetDevice."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=cgutils.voidptr_t,
+        arg_list=[cgutils.voidptr_t],
+        func_name="DPCTLQueue_GetDevice",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+def dpctl_queue_submit_range(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLQueue_SubmitRange.
+
+    DPCTLSyclEventRef
+    DPCTLQueue_SubmitRange(
+        const DPCTLSyclKernelRef KRef,
+        const DPCTLSyclQueueRef QRef,
+        void** Args,
+        const DPCTLKernelArgType* ArgTypes,
+        size_t NArgs,
+        const size_t Range[3],
+        size_t NRange,
+        const DPCTLSyclEventRef* DepEvents,
+        size_t NDepEvents
+    );
+
+    """
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=cgutils.voidptr_t,
+        arg_list=[
+            cgutils.voidptr_t,
+            cgutils.voidptr_t,
+            cgutils.voidptr_t.as_pointer(),
+            cgutils.int32_t.as_pointer(),
+            llvmir.IntType(64),
+            llvmir.IntType(64).as_pointer(),
+            llvmir.IntType(64),
+            cgutils.voidptr_t.as_pointer(),
+            llvmir.IntType(64),
+        ],
+        func_name="DPCTLQueue_SubmitRange",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+def dpctl_queue_submit_ndrange(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLQueue_SubmitNDRange.
+
+    DPCTLSyclEventRef
+    DPCTLQueue_SubmitNDRange(
+        const DPCTLSyclKernelRef KRef,
+        const DPCTLSyclQueueRef QRef,
+        void** Args,
+        const DPCTLKernelArgType* ArgTypes,
+        size_t NArgs,
+        const size_t gRange[3],
+        const size_t lRange[3],
+        size_t NDims,
+        const DPCTLSyclEventRef* DepEvents,
+        size_t NDepEvents
+    );
+
+    """
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=cgutils.voidptr_t,
+        arg_list=[
+            cgutils.voidptr_t,
+            cgutils.voidptr_t,
+            cgutils.voidptr_t.as_pointer(),
+            cgutils.int32_t.as_pointer(),
+            llvmir.IntType(64),
+            llvmir.IntType(64).as_pointer(),
+            llvmir.IntType(64).as_pointer(),
+            llvmir.IntType(64),
+            cgutils.voidptr_t.as_pointer(),
+            llvmir.IntType(64),
+        ],
+        func_name="DPCTLQueue_SubmitNDRange",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+# --------------------- DpctlSyclDevice functions -----------------------------#
+
+
+def dpctl_device_delete(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLDevice_Delete."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=llvmir.types.VoidType(),
+        arg_list=[cgutils.voidptr_t],
+        func_name="DPCTLDevice_Delete",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+# --------------------- DpctlSyclKernelBundle functions -----------------------#
+
+
+def dpctl_kernel_bundle_create_from_spirv(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLKernelBundle_CreateFromSpirv."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=cgutils.voidptr_t,
+        arg_list=[
+            cgutils.voidptr_t,
+            cgutils.voidptr_t,
+            cgutils.voidptr_t,
+            llvmir.IntType(64),
+            cgutils.voidptr_t,
+        ],
+        func_name="DPCTLKernelBundle_CreateFromSpirv",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+def dpctl_kernel_bundle_delete(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLKernelBundle_Delete."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=llvmir.types.VoidType(),
+        arg_list=[cgutils.voidptr_t],
+        func_name="DPCTLKernelBundle_Delete",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+def dpctl_kernel_bundle_get_kernel(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLKernelBundle_GetKernel."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=cgutils.voidptr_t,
+        arg_list=[cgutils.voidptr_t, cgutils.voidptr_t],
+        func_name="DPCTLKernelBundle_GetKernel",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+# --------------------- DpctlSyclKernel functions -----------------------------#
+
+
+def dpctl_kernel_delete(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLKernel_Delete."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=llvmir.types.VoidType(),
+        arg_list=[cgutils.voidptr_t],
+        func_name="DPCTLKernel_Delete",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+# --------------------- DpctlSyclContext functions ----------------------------#
+
+
+def dpctl_context_delete(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLContext_Delete."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=llvmir.types.VoidType(),
+        arg_list=[cgutils.voidptr_t],
+        func_name="DPCTLContext_Delete",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+# --------------------- DpctlSyclEvent functions ------------------------------#
+
+
+def dpctl_event_wait(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLEvent_Wait."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=llvmir.VoidType(),
+        arg_list=[cgutils.voidptr_t],
+        func_name="DPCTLEvent_Wait",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
+
+
+def dpctl_event_delete(builder: llvmir.IRBuilder, *args):
+    """Inserts LLVM IR to call DPCTLEvent_Delete."""
+    mod = builder.module
+    fn = _build_dpctl_function(
+        llvm_module=mod,
+        return_ty=llvmir.VoidType(),
+        arg_list=[cgutils.voidptr_t],
+        func_name="DPCTLEvent_Delete",
+    )
+    ret = builder.call(fn, args)
+
+    return ret
diff --git a/numba_dpex/dpctl_iface/wrappers.py b/numba_dpex/dpctl_iface/wrappers.py
new file mode 100644
index 0000000000..d5dcfb6dcb
--- /dev/null
+++ b/numba_dpex/dpctl_iface/wrappers.py
@@ -0,0 +1,35 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from numba.core import cgutils
+
+from numba_dpex.core.runtime import context as dpexrt
+from numba_dpex.core.types import DpctlSyclEvent
+
+
+def wrap_event_reference(ctx, builder, eref):
+    """Wrap dpctl event reference into datamodel so it can be boxed to
+    Python."""
+
+    ty_event = DpctlSyclEvent()
+
+    pyapi = ctx.get_python_api(builder)
+
+    event_struct_proxy = cgutils.create_struct_proxy(ty_event)(ctx, builder)
+
+    # Ref count after the call is equal to 1.
+    # TODO: get dpex RT from cached property once the PR is merged
+    # https://github.com/IntelPython/numba-dpex/pull/1027
+    # ctx.dpexrt.eventstruct_init( # noqa: W0621
+    dpexrt.DpexRTContext(ctx).eventstruct_init(
+        pyapi,
+        eref,
+        # calling _<method>() is by numba's design
+        event_struct_proxy._getpointer(),  # pylint: disable=W0212
+    )
+
+    # calling _<method>() is by numba's design
+    event_value = event_struct_proxy._getvalue()  # pylint: disable=W0212
+
+    return event_value
diff --git a/numba_dpex/dpnp_iface/__init__.py b/numba_dpex/dpnp_iface/__init__.py
index d4e46283c8..da3defe7e7 100644
--- a/numba_dpex/dpnp_iface/__init__.py
+++ b/numba_dpex/dpnp_iface/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/dpnp_iface/_intrinsic.py b/numba_dpex/dpnp_iface/_intrinsic.py
index a540a13cb0..d37402079b 100644
--- a/numba_dpex/dpnp_iface/_intrinsic.py
+++ b/numba_dpex/dpnp_iface/_intrinsic.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -6,11 +6,12 @@
 
 from dpctl import get_device_cached_queue
 from llvmlite import ir as llvmir
-from llvmlite.ir import Constant
+from llvmlite.ir import Constant, IRBuilder
 from llvmlite.ir.types import DoubleType, FloatType
 from numba import types
 from numba.core import cgutils
 from numba.core import config as numba_config
+from numba.core import errors, imputils
 from numba.core.typing import signature
 from numba.extending import intrinsic, overload_classmethod
 from numba.np.arrayobj import (
@@ -21,10 +22,13 @@
     populate_array,
 )
 
-from numba_dpex.core.datamodel.models import dpex_data_model_manager as dpex_dmm
 from numba_dpex.core.runtime import context as dpexrt
 from numba_dpex.core.types import DpnpNdArray
 from numba_dpex.core.types.dpctl_types import DpctlSyclQueue
+from numba_dpex.dpctl_iface import libsyclinterface_bindings as sycl
+
+# can't import name because of the circular import
+DPEX_TARGET_NAME = "dpex"
 
 _QueueRefPayload = namedtuple(
     "QueueRefPayload", ["queue_ref", "py_dpctl_sycl_queue_addr", "pyapi"]
@@ -72,8 +76,7 @@ def make_queue(context, builder, py_dpctl_sycl_queue):
         pyapi, py_dpctl_sycl_queue_addr, queue_struct_voidptr
     )
 
-    queue_struct = builder.load(queue_struct_ptr)
-    queue_ref = builder.extract_value(queue_struct, 1)
+    queue_ref = queue_struct_proxy.queue_ref
 
     return_values = namedtuple(
         "return_values", "queue_ref queue_address_ptr pyapi"
@@ -138,13 +141,15 @@ def _get_queue_ref(
             raise AssertionError(
                 "Expected the queue_arg to be an llvmir.LiteralStructType"
             )
-        sycl_queue_dm = dpex_dmm.lookup(sycl_queue_arg.numba_ty)
+        sycl_queue_dm = context.data_model_manager.lookup(
+            sycl_queue_arg.numba_ty
+        )
         queue_ref = builder.extract_value(
             sycl_queue_arg.llvmir_val,
             sycl_queue_dm.get_field_position("queue_ref"),
         )
     elif array_arg is not None:
-        dpnp_ndarray_dm = dpex_dmm.lookup(array_arg.numba_ty)
+        dpnp_ndarray_dm = context.data_model_manager.lookup(array_arg.numba_ty)
         queue_ref = builder.extract_value(
             array_arg.llvmir_val,
             dpnp_ndarray_dm.get_field_position("sycl_queue"),
@@ -155,9 +160,14 @@ def _get_queue_ref(
             raise AssertionError(
                 "Expected the queue_arg to be an llvmir.PointerType"
             )
+        # We are emulating queue boxing here to pass it as an argument.
+        # TODO: do we have a better way?
         py_dpctl_sycl_queue = get_device_cached_queue(
             returned_sycl_queue_ty.sycl_device
         )
+        # TODO: make sure that queue is not getting garbage collected between
+        # lowering and executing.
+        # cache prevent it from happening but it is not clean way
         (queue_ref, py_dpctl_sycl_queue_addr, pyapi) = make_queue(
             context, builder, py_dpctl_sycl_queue
         )
@@ -248,8 +258,7 @@ def _empty_nd_impl(context, builder, arrtype, shapes, queue_ref):
     # collected. Whereas, the copied queue_ref is to be owned by the
     # NRT_External_Allocator object of MemInfo, and its lifetime is tied to the
     # MemInfo object.
-    dpexrtCtx = dpexrt.DpexRTContext(context)
-    queue_ref_copy = dpexrtCtx.copy_queue(builder, queue_ref)
+    queue_ref_copy = sycl.dpctl_queue_copy(builder, queue_ref)
 
     usm_ty = arrtype.usm_type
     usm_ty_map = {"device": 1, "shared": 2, "host": 3}
@@ -300,7 +309,7 @@ def _empty_nd_impl(context, builder, arrtype, shapes, queue_ref):
     return ary
 
 
-@overload_classmethod(DpnpNdArray, "_usm_allocate")
+@overload_classmethod(DpnpNdArray, "_usm_allocate", target=DPEX_TARGET_NAME)
 def _ol_array_allocate(cls, allocsize, usm_type, queue):
     """Implements an allocator for dpnp.ndarrays."""
 
@@ -321,7 +330,7 @@ def _call_usm_allocator(arrtype, size, usm_type, queue):
 numba_config.DISABLE_PERFORMANCE_WARNINGS = 1
 
 
-@intrinsic
+@intrinsic(target=DPEX_TARGET_NAME)
 def intrin_usm_alloc(typingctx, allocsize, usm_type, queue):
     """Intrinsic to call into the allocator for Array"""
 
@@ -420,7 +429,7 @@ def fill_arrayobj(context, builder, ary, arrtype, queue_ref, fill_value):
     return ary, arrtype
 
 
-@intrinsic
+@intrinsic(target=DPEX_TARGET_NAME)
 def impl_dpnp_empty(
     ty_context,
     ty_shape,
@@ -485,15 +494,12 @@ def codegen(context, builder, sig, args):
             context, builder, sig, qref_payload.queue_ref, args
         )
 
-        if qref_payload.py_dpctl_sycl_queue_addr:
-            qref_payload.pyapi.decref(qref_payload.py_dpctl_sycl_queue_addr)
-
         return ary._getvalue()
 
     return sig, codegen
 
 
-@intrinsic
+@intrinsic(target=DPEX_TARGET_NAME)
 def impl_dpnp_zeros(
     ty_context,
     ty_shape,
@@ -564,15 +570,13 @@ def codegen(context, builder, sig, args):
             qref_payload.queue_ref,
             fill_value,
         )
-        if qref_payload.py_dpctl_sycl_queue_addr:
-            qref_payload.pyapi.decref(qref_payload.py_dpctl_sycl_queue_addr)
 
         return ary._getvalue()
 
     return sig, codegen
 
 
-@intrinsic
+@intrinsic(target=DPEX_TARGET_NAME)
 def impl_dpnp_ones(
     ty_context,
     ty_shape,
@@ -644,15 +648,13 @@ def codegen(context, builder, sig, args):
             qref_payload.queue_ref,
             fill_value,
         )
-        if qref_payload.py_dpctl_sycl_queue_addr:
-            qref_payload.pyapi.decref(qref_payload.py_dpctl_sycl_queue_addr)
 
         return ary._getvalue()
 
     return sig, codegen
 
 
-@intrinsic
+@intrinsic(target=DPEX_TARGET_NAME)
 def impl_dpnp_full(
     ty_context,
     ty_shape,
@@ -730,15 +732,13 @@ def codegen(context, builder, sig, args):
             qref_payload.queue_ref,
             fill_value,
         )
-        if qref_payload.py_dpctl_sycl_queue_addr:
-            qref_payload.pyapi.decref(qref_payload.py_dpctl_sycl_queue_addr)
 
         return ary._getvalue()
 
     return signature, codegen
 
 
-@intrinsic
+@intrinsic(target=DPEX_TARGET_NAME)
 def impl_dpnp_empty_like(
     ty_context,
     ty_x1,
@@ -812,15 +812,12 @@ def codegen(context, builder, sig, args):
             context, builder, sig, qref_payload.queue_ref, args, is_like=True
         )
 
-        if qref_payload.py_dpctl_sycl_queue_addr:
-            qref_payload.pyapi.decref(qref_payload.py_dpctl_sycl_queue_addr)
-
         return ary._getvalue()
 
     return sig, codegen
 
 
-@intrinsic
+@intrinsic(target=DPEX_TARGET_NAME)
 def impl_dpnp_zeros_like(
     ty_context,
     ty_x1,
@@ -902,15 +899,13 @@ def codegen(context, builder, sig, args):
             qref_payload.queue_ref,
             fill_value,
         )
-        if qref_payload.py_dpctl_sycl_queue_addr:
-            qref_payload.pyapi.decref(qref_payload.py_dpctl_sycl_queue_addr)
 
         return ary._getvalue()
 
     return sig, codegen
 
 
-@intrinsic
+@intrinsic(target=DPEX_TARGET_NAME)
 def impl_dpnp_ones_like(
     ty_context,
     ty_x1,
@@ -991,15 +986,13 @@ def codegen(context, builder, sig, args):
             qref_payload.queue_ref,
             fill_value,
         )
-        if qref_payload.py_dpctl_sycl_queue_addr:
-            qref_payload.pyapi.decref(qref_payload.py_dpctl_sycl_queue_addr)
 
         return ary._getvalue()
 
     return sig, codegen
 
 
-@intrinsic
+@intrinsic(target=DPEX_TARGET_NAME)
 def impl_dpnp_full_like(
     ty_context,
     ty_x1,
@@ -1084,9 +1077,65 @@ def codegen(context, builder, sig, args):
             qref_payload.queue_ref,
             fill_value,
         )
-        if qref_payload.py_dpctl_sycl_queue_addr:
-            qref_payload.pyapi.decref(qref_payload.py_dpctl_sycl_queue_addr)
 
         return ary._getvalue()
 
     return signature, codegen
+
+
+@intrinsic(target=DPEX_TARGET_NAME)
+def ol_dpnp_nd_array_sycl_queue(
+    ty_context,
+    ty_dpnp_nd_array: DpnpNdArray,
+):
+    if not isinstance(ty_dpnp_nd_array, DpnpNdArray):
+        raise errors.TypingError("Argument must be DpnpNdArray")
+
+    ty_queue: DpctlSyclQueue = ty_dpnp_nd_array.queue
+
+    sig = ty_queue(ty_dpnp_nd_array)
+
+    def codegen(context, builder: IRBuilder, sig, args: list):
+        array_proxy = cgutils.create_struct_proxy(ty_dpnp_nd_array)(
+            context,
+            builder,
+            value=args[0],
+        )
+
+        queue_ref = array_proxy.sycl_queue
+
+        queue_struct_proxy = cgutils.create_struct_proxy(ty_queue)(
+            context, builder
+        )
+
+        queue_struct_proxy.queue_ref = queue_ref
+        queue_struct_proxy.meminfo = array_proxy.meminfo
+
+        # Warning: current implementation prevents whole object from being
+        # destroyed as long as sycl_queue attribute is being used. It should be
+        # okay since anywere we use it as an argument callee creates a copy
+        # so it does not steel reference.
+        #
+        # We can avoid it by:
+        #  queue_ref_copy = sycl.dpctl_queue_copy(builder, queue_ref) #noqa E800
+        #  queue_struct_proxy.queue_ref = queue_ref_copy #noqa E800
+        #  queue_struct->meminfo =
+        #     nrt->manage_memory(queue_ref_copy, DPCTLEvent_Delete);
+        # but it will allocate new meminfo object which can negatively affect
+        # performance.
+        # Speaking philosophically attribute is a part of the object and as long
+        # as nobody can still the reference it is a part of the owner object
+        # and lifetime is tied to it.
+        # TODO: we want to have queue: queuestruct_t instead of
+        #   queue_ref: QueueRef as an attribute for DPNPNdArray.
+
+        queue_value = queue_struct_proxy._getvalue()
+
+        # We need to incref meminfo so that queue model is preventing parent
+        # ndarray from being destroyed, that can destroy queue that we are
+        # using.
+        return imputils.impl_ret_borrowed(
+            context, builder, ty_queue, queue_value
+        )
+
+    return sig, codegen
diff --git a/numba_dpex/dpnp_iface/arrayobj.py b/numba_dpex/dpnp_iface/arrayobj.py
index c6aa3d7978..2e0e02e2a3 100644
--- a/numba_dpex/dpnp_iface/arrayobj.py
+++ b/numba_dpex/dpnp_iface/arrayobj.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -6,16 +6,20 @@
 
 import dpnp
 from numba import errors, types
-from numba.core.imputils import lower_builtin
+from numba.core.imputils import impl_ret_borrowed, lower_builtin
 from numba.core.types import scalars
 from numba.core.types.containers import UniTuple
 from numba.core.typing.npydecl import parse_dtype as _ty_parse_dtype
 from numba.core.typing.npydecl import parse_shape as _ty_parse_shape
-from numba.extending import overload
-from numba.np.arrayobj import getitem_arraynd_intp as np_getitem_arraynd_intp
+from numba.extending import overload, overload_attribute
+from numba.np.arrayobj import _getitem_array_generic as np_getitem_array_generic
+from numba.np.arrayobj import make_array
 from numba.np.numpy_support import is_nonelike
 
-from numba_dpex.core.datamodel.models import dpex_data_model_manager as dpex_dmm
+from numba_dpex.core.kernel_interface.arrayobj import (
+    _getitem_array_generic as kernel_getitem_array_generic,
+)
+from numba_dpex.core.targets.kernel_target import DpexKernelTargetContext
 from numba_dpex.core.types import DpnpNdArray
 
 from ._intrinsic import (
@@ -27,8 +31,12 @@
     impl_dpnp_ones_like,
     impl_dpnp_zeros,
     impl_dpnp_zeros_like,
+    ol_dpnp_nd_array_sycl_queue,
 )
 
+# can't import name because of the circular import
+DPEX_TARGET_NAME = "dpex"
+
 # =========================================================================
 #               Helps to parse dpnp constructor arguments
 # =========================================================================
@@ -163,7 +171,7 @@ def _parse_device_filter_string(device):
 # =========================================================================
 
 
-@overload(dpnp.empty, prefer_literal=True)
+@overload(dpnp.empty, prefer_literal=True, target=DPEX_TARGET_NAME)
 def ol_dpnp_empty(
     shape,
     dtype=None,
@@ -234,7 +242,7 @@ def impl(
                 shape,
                 dtype=None,
                 order="C",
-                # like=None, # see issue https://github.com/IntelPython/numba-dpex/issues/998
+                # like=None, # noqa: E800 see issue https://github.com/IntelPython/numba-dpex/issues/998
                 device=None,
                 usm_type="device",
                 sycl_queue=None,
@@ -243,7 +251,7 @@ def impl(
                     shape,
                     _dtype,
                     order,
-                    # like, # see issue https://github.com/IntelPython/numba-dpex/issues/998
+                    # like, # noqa: E800 see issue https://github.com/IntelPython/numba-dpex/issues/998
                     _device,
                     _usm_type,
                     sycl_queue,
@@ -260,7 +268,7 @@ def impl(
         raise errors.TypingError("Could not infer the rank of the ndarray.")
 
 
-@overload(dpnp.zeros, prefer_literal=True)
+@overload(dpnp.zeros, prefer_literal=True, target=DPEX_TARGET_NAME)
 def ol_dpnp_zeros(
     shape,
     dtype=None,
@@ -354,7 +362,7 @@ def impl(
         raise errors.TypingError("Could not infer the rank of the ndarray.")
 
 
-@overload(dpnp.ones, prefer_literal=True)
+@overload(dpnp.ones, prefer_literal=True, target=DPEX_TARGET_NAME)
 def ol_dpnp_ones(
     shape,
     dtype=None,
@@ -448,7 +456,7 @@ def impl(
         raise errors.TypingError("Could not infer the rank of the ndarray.")
 
 
-@overload(dpnp.full, prefer_literal=True)
+@overload(dpnp.full, prefer_literal=True, target=DPEX_TARGET_NAME)
 def ol_dpnp_full(
     shape,
     fill_value,
@@ -557,7 +565,7 @@ def impl(
         raise errors.TypingError("Could not infer the rank of the ndarray.")
 
 
-@overload(dpnp.empty_like, prefer_literal=True)
+@overload(dpnp.empty_like, prefer_literal=True, target=DPEX_TARGET_NAME)
 def ol_dpnp_empty_like(
     x1,
     dtype=None,
@@ -682,7 +690,7 @@ def impl(
         )
 
 
-@overload(dpnp.zeros_like, prefer_literal=True)
+@overload(dpnp.zeros_like, prefer_literal=True, target=DPEX_TARGET_NAME)
 def ol_dpnp_zeros_like(
     x1,
     dtype=None,
@@ -806,7 +814,7 @@ def impl(
         )
 
 
-@overload(dpnp.ones_like, prefer_literal=True)
+@overload(dpnp.ones_like, prefer_literal=True, target=DPEX_TARGET_NAME)
 def ol_dpnp_ones_like(
     x1,
     dtype=None,
@@ -931,7 +939,7 @@ def impl(
         )
 
 
-@overload(dpnp.full_like, prefer_literal=True)
+@overload(dpnp.full_like, prefer_literal=True, target=DPEX_TARGET_NAME)
 def ol_dpnp_full_like(
     x1,
     fill_value,
@@ -1061,6 +1069,7 @@ def impl(
         )
 
 
+# TODO: target specific
 @lower_builtin(operator.getitem, DpnpNdArray, types.Integer)
 @lower_builtin(operator.getitem, DpnpNdArray, types.SliceType)
 def getitem_arraynd_intp(context, builder, sig, args):
@@ -1073,15 +1082,50 @@ def getitem_arraynd_intp(context, builder, sig, args):
     that when returning a view of a dpnp.ndarray the sycl::queue pointer
     member in the LLVM IR struct gets properly updated.
     """
-    ret = np_getitem_arraynd_intp(context, builder, sig, args)
+    getitem_call_in_kernel = isinstance(context, DpexKernelTargetContext)
+    _getitem_array_generic = np_getitem_array_generic
+
+    if getitem_call_in_kernel:
+        _getitem_array_generic = kernel_getitem_array_generic
+
+    aryty, idxty = sig.args
+    ary, idx = args
+
+    assert aryty.ndim >= 1
+    ary = make_array(aryty)(context, builder, ary)
 
-    if isinstance(sig.return_type, DpnpNdArray):
+    res = _getitem_array_generic(
+        context, builder, sig.return_type, aryty, ary, (idxty,), (idx,)
+    )
+    ret = impl_ret_borrowed(context, builder, sig.return_type, res)
+
+    if isinstance(sig.return_type, DpnpNdArray) and not getitem_call_in_kernel:
         array_val = args[0]
         array_ty = sig.args[0]
-        sycl_queue_attr_pos = dpex_dmm.lookup(array_ty).get_field_position(
-            "sycl_queue"
-        )
+        sycl_queue_attr_pos = context.data_model_manager.lookup(
+            array_ty
+        ).get_field_position("sycl_queue")
         sycl_queue_attr = builder.extract_value(array_val, sycl_queue_attr_pos)
         ret = builder.insert_value(ret, sycl_queue_attr, sycl_queue_attr_pos)
 
     return ret
+
+
+@overload_attribute(DpnpNdArray, "sycl_queue", target=DPEX_TARGET_NAME)
+def dpnp_nd_array_sycl_queue(arr):
+    """Returns :class:`dpctl.SyclQueue` object associated with USM data.
+
+    This is an overloaded attribute implementation for dpnp.sycl_queue.
+
+    Args:
+        arr (numba_dpex.core.types.DpnpNdArray): Input array from which to
+            take sycl_queue.
+
+    Returns:
+        function: Local function `ol_dpnp_nd_array_sycl_queue()`.
+    """
+
+    def get(arr):
+        return ol_dpnp_nd_array_sycl_queue(arr)
+
+    return get
diff --git a/numba_dpex/dpnp_iface/dpnp_ufunc_db.py b/numba_dpex/dpnp_iface/dpnp_ufunc_db.py
index 6a6d3d6d50..5731bcaa32 100644
--- a/numba_dpex/dpnp_iface/dpnp_ufunc_db.py
+++ b/numba_dpex/dpnp_iface/dpnp_ufunc_db.py
@@ -1,7 +1,8 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
+import copy
 
 import dpnp
 import numpy as np
@@ -11,23 +12,50 @@
 
 from ..ocl import mathimpl
 
+# A global instance of dpnp ufuncs that are supported by numba-dpex
+_dpnp_ufunc_db = None
+
+
+def _lazy_init_dpnp_db():
+    global _dpnp_ufunc_db
+
+    if _dpnp_ufunc_db is None:
+        _dpnp_ufunc_db = {}
+        _fill_ufunc_db_with_dpnp_ufuncs(_dpnp_ufunc_db)
+
 
 def get_ufuncs():
-    """obtain a list of supported ufuncs in the db"""
+    """Returns the list of supported dpnp ufuncs in the _dpnp_ufunc_db"""
 
-    from numba.np.ufunc_db import _lazy_init_db
+    _lazy_init_dpnp_db()
+
+    return _dpnp_ufunc_db.keys()
 
-    _lazy_init_db()
-    from numba.np.ufunc_db import _ufunc_db
 
-    _fill_ufunc_db_with_dpnp_ufuncs(_ufunc_db)
+def get_ufunc_info(ufunc_key):
+    """get the lowering information for the ufunc with key ufunc_key.
 
-    return _ufunc_db.keys()
+    The lowering information is a dictionary that maps from a numpy
+    loop string (as given by the ufunc types attribute) to a function
+    that handles code generation for a scalar version of the ufunc
+    (that is, generates the "per element" operation").
+
+    raises a KeyError if the ufunc is not in the ufunc_db
+    """
+    _lazy_init_dpnp_db()
+    return _dpnp_ufunc_db[ufunc_key]
 
 
 def _fill_ufunc_db_with_dpnp_ufuncs(ufunc_db):
-    """Monkey patching dpnp for missing attributes."""
-    # FIXME: add more docstring
+    """Populates the _dpnp_ufunc_db from Numba's NumPy ufunc_db"""
+
+    from numba.np.ufunc_db import _lazy_init_db
+
+    _lazy_init_db()
+
+    # we need to import it after, because before init it is None and
+    # variable is passed by value
+    from numba.np.ufunc_db import _ufunc_db
 
     for ufuncop in dpnpdecl.supported_ufuncs:
         if ufuncop == "erf":
@@ -52,7 +80,12 @@ def _fill_ufunc_db_with_dpnp_ufuncs(ufunc_db):
             op.nargs = npop.nargs
             op.types = npop.types
             op.is_dpnp_ufunc = True
-            ufunc_db.update({op: ufunc_db[npop]})
+            cp = copy.copy(_ufunc_db[npop])
+            if "'divide'" in str(npop):
+                # TODO: why do we need to do it only for divide?
+                # https://github.com/IntelPython/numba-dpex/issues/1270
+                ufunc_db.update({npop: cp})
+            ufunc_db.update({op: cp})
             for key in list(ufunc_db[op].keys()):
                 if (
                     "FF->" in key
diff --git a/numba_dpex/dpnp_iface/dpnpimpl.py b/numba_dpex/dpnp_iface/dpnpimpl.py
index da2a2b6cb8..1313b26a8b 100644
--- a/numba_dpex/dpnp_iface/dpnpimpl.py
+++ b/numba_dpex/dpnp_iface/dpnpimpl.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -8,7 +8,7 @@
 from numba.core.imputils import Registry
 from numba.np import npyimpl
 
-from numba_dpex.core.typing.dpnpdecl import _unsupported
+from numba_dpex.core.typing import dpnpdecl
 from numba_dpex.dpnp_iface import dpnp_ufunc_db
 
 registry = Registry("dpnpimpl")
@@ -36,11 +36,11 @@ def _register_dpnp_ufuncs():
         )
 
     for _op_map in (
-        npyimpl.npydecl.NumpyRulesUnaryArrayOperator._op_map,
-        npyimpl.npydecl.NumpyRulesArrayOperator._op_map,
+        dpnpdecl.DpnpRulesUnaryArrayOperator._op_map,
+        dpnpdecl.DpnpRulesArrayOperator._op_map,
     ):
         for operator, ufunc_name in _op_map.items():
-            if ufunc_name in _unsupported:
+            if ufunc_name in dpnpdecl._unsupported:
                 continue
             ufunc = getattr(dpnp, ufunc_name)
             kernel = kernels[ufunc]
@@ -57,9 +57,9 @@ def _register_dpnp_ufuncs():
                     "There shouldn't be any non-unary or binary operators"
                 )
 
-    for _op_map in (npyimpl.npydecl.NumpyRulesInplaceArrayOperator._op_map,):
+    for _op_map in (dpnpdecl.DpnpRulesInplaceArrayOperator._op_map,):
         for operator, ufunc_name in _op_map.items():
-            if ufunc_name in _unsupported:
+            if ufunc_name in dpnpdecl._unsupported:
                 continue
             ufunc = getattr(dpnp, ufunc_name)
             kernel = kernels[ufunc]
diff --git a/numba_dpex/examples/_helper.py b/numba_dpex/examples/_helper.py
index 575050c1e4..00e7a5ab56 100644
--- a/numba_dpex/examples/_helper.py
+++ b/numba_dpex/examples/_helper.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/blacksholes_njit.py b/numba_dpex/examples/blacksholes_njit.py
index 9095f79987..eed04b4241 100644
--- a/numba_dpex/examples/blacksholes_njit.py
+++ b/numba_dpex/examples/blacksholes_njit.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -12,11 +12,6 @@
 
 import numba_dpex as dpex
 
-# @numba.vectorize(nopython=True)
-# def cndf2(inp):
-#     out = 0.5 + 0.5 * math.erf((math.sqrt(2.0) / 2.0) * inp)
-#     return out
-
 
 @dpex.dpjit
 def blackscholes(sptprice, strike, timev, rate, volatility):
@@ -68,7 +63,6 @@ def run(iterations):
     t1 = time.time()
     put = blackscholes(sptprice, initStrike, rate, volatility, timev)
     t = time.time() - t1
-    #  print("checksum: ", sum(put))
     print(put)
     print("SELFTIMED ", t)
 
diff --git a/numba_dpex/examples/debug/dpex_func.py b/numba_dpex/examples/debug/dpex_func.py
index 69ac5f2f1b..321eaeb004 100644
--- a/numba_dpex/examples/debug/dpex_func.py
+++ b/numba_dpex/examples/debug/dpex_func.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/debug/njit_basic.py b/numba_dpex/examples/debug/njit_basic.py
index cbe1ab19f1..a904351e3e 100644
--- a/numba_dpex/examples/debug/njit_basic.py
+++ b/numba_dpex/examples/debug/njit_basic.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/debug/side-by-side-2.py b/numba_dpex/examples/debug/side-by-side-2.py
index c7bdbf163e..ce0144d7a6 100644
--- a/numba_dpex/examples/debug/side-by-side-2.py
+++ b/numba_dpex/examples/debug/side-by-side-2.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/debug/side-by-side.py b/numba_dpex/examples/debug/side-by-side.py
index 7325e9d3c8..514df9c9fa 100644
--- a/numba_dpex/examples/debug/side-by-side.py
+++ b/numba_dpex/examples/debug/side-by-side.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/debug/simple_dpex_func.py b/numba_dpex/examples/debug/simple_dpex_func.py
index 681b51945d..a7e2c241cb 100644
--- a/numba_dpex/examples/debug/simple_dpex_func.py
+++ b/numba_dpex/examples/debug/simple_dpex_func.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/debug/simple_sum.py b/numba_dpex/examples/debug/simple_sum.py
index 076af47130..bcdb2f0c98 100644
--- a/numba_dpex/examples/debug/simple_sum.py
+++ b/numba_dpex/examples/debug/simple_sum.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/debug/sum.py b/numba_dpex/examples/debug/sum.py
index 6dd1136dee..9974a65a96 100644
--- a/numba_dpex/examples/debug/sum.py
+++ b/numba_dpex/examples/debug/sum.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/debug/sum_local_vars.py b/numba_dpex/examples/debug/sum_local_vars.py
index 1a7d0b58f8..537aab40ce 100644
--- a/numba_dpex/examples/debug/sum_local_vars.py
+++ b/numba_dpex/examples/debug/sum_local_vars.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/debug/sum_local_vars_revive.py b/numba_dpex/examples/debug/sum_local_vars_revive.py
index d5784af6ed..11b53d99f3 100644
--- a/numba_dpex/examples/debug/sum_local_vars_revive.py
+++ b/numba_dpex/examples/debug/sum_local_vars_revive.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/dpjit/vector_sum.py b/numba_dpex/examples/dpjit/vector_sum.py
index 189bf0cda6..342ce0d0c4 100644
--- a/numba_dpex/examples/dpjit/vector_sum.py
+++ b/numba_dpex/examples/dpjit/vector_sum.py
@@ -1,4 +1,4 @@
-# Copyright 2020 - 2023 Intel Corporation
+# Copyright 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/dpjit/vector_sum2D.py b/numba_dpex/examples/dpjit/vector_sum2D.py
index 5cc62ad6f8..07b60d5134 100644
--- a/numba_dpex/examples/dpjit/vector_sum2D.py
+++ b/numba_dpex/examples/dpjit/vector_sum2D.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/atomic_op.py b/numba_dpex/examples/kernel/atomic_op.py
index 49a5d74671..f0e22aa6aa 100644
--- a/numba_dpex/examples/kernel/atomic_op.py
+++ b/numba_dpex/examples/kernel/atomic_op.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/black_scholes.py b/numba_dpex/examples/kernel/black_scholes.py
index 114eb5a506..b139371d87 100644
--- a/numba_dpex/examples/kernel/black_scholes.py
+++ b/numba_dpex/examples/kernel/black_scholes.py
@@ -1,6 +1,7 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache 2.0
+# SPDX-License-Identifier: Apache-2.0
 
 from math import erf, exp, log, sqrt
 
diff --git a/numba_dpex/examples/kernel/device_func.py b/numba_dpex/examples/kernel/device_func.py
index 508008ec6c..c178999157 100644
--- a/numba_dpex/examples/kernel/device_func.py
+++ b/numba_dpex/examples/kernel/device_func.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/interpolation.py b/numba_dpex/examples/kernel/interpolation.py
index a9536c410b..cd023c69b6 100644
--- a/numba_dpex/examples/kernel/interpolation.py
+++ b/numba_dpex/examples/kernel/interpolation.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/kernel_private_memory.py b/numba_dpex/examples/kernel/kernel_private_memory.py
index e0e3e7c5dc..249274c002 100644
--- a/numba_dpex/examples/kernel/kernel_private_memory.py
+++ b/numba_dpex/examples/kernel/kernel_private_memory.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/kernel_specialization.py b/numba_dpex/examples/kernel/kernel_specialization.py
index ccd453afa0..7367ec380f 100644
--- a/numba_dpex/examples/kernel/kernel_specialization.py
+++ b/numba_dpex/examples/kernel/kernel_specialization.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/matmul.py b/numba_dpex/examples/kernel/matmul.py
index 03fe2b98de..804dd3b16e 100644
--- a/numba_dpex/examples/kernel/matmul.py
+++ b/numba_dpex/examples/kernel/matmul.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/pairwise_distance.py b/numba_dpex/examples/kernel/pairwise_distance.py
index b52b43bdb0..0bfb6205d7 100644
--- a/numba_dpex/examples/kernel/pairwise_distance.py
+++ b/numba_dpex/examples/kernel/pairwise_distance.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/pipelining.py b/numba_dpex/examples/kernel/pipelining.py
new file mode 100644
index 0000000000..4272ed5b26
--- /dev/null
+++ b/numba_dpex/examples/kernel/pipelining.py
@@ -0,0 +1,147 @@
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+The overlapping of memory copy and computation depends on both the complexity of
+the computation and the device hardware specifications such as memory bandwidth.
+The example serves as illustration of how memory copy and compute can be
+overlapped using numba_dpex's async kernel execution feature. For real world use
+cases, the compute kernel will need proper profiling and the pipelining of the
+compute and memory copy kernels will need to be tailored per device
+capabilities.
+"""
+
+import argparse
+import time
+
+import dpctl
+import dpnp
+import numpy as np
+
+import numba_dpex as dpex
+import numba_dpex.experimental as dpex_exp
+
+
+@dpex_exp.kernel
+def async_kernel(x):
+    idx = dpex.get_global_id(0)
+
+    for i in range(1300):
+        den = x.dtype.type(i + 1)
+        x[idx] += x.dtype.type(1) / (den * den * den)
+
+
+def run_serial(host_arr, n_itr):
+    t0 = time.time()
+    q = dpctl.SyclQueue()
+
+    a_host = dpnp.asarray(host_arr, usm_type="host", sycl_queue=q)
+    usm_host_data = dpnp.get_usm_ndarray(a_host).usm_data
+
+    batch_shape = (n_itr,) + a_host.shape
+    device_alloc = dpnp.empty(batch_shape, usm_type="device", sycl_queue=q)
+
+    for offset in range(n_itr):
+        _a = device_alloc[offset]
+        _a_data = dpnp.get_usm_ndarray(_a).usm_data
+
+        q.memcpy(_a_data, usm_host_data, usm_host_data.nbytes)
+
+        dpex_exp.call_kernel(
+            async_kernel,
+            dpex.Range(len(_a)),
+            _a,
+        )
+
+    dt = time.time() - t0
+
+    return dt, None, None
+
+
+def run_pipeline(host_arr, n_itr):
+    t0 = time.time()
+    q = dpctl.SyclQueue()
+
+    a_host = dpnp.asarray(host_arr, usm_type="host", sycl_queue=q)
+    usm_host_data = dpnp.get_usm_ndarray(a_host).usm_data
+
+    batch_shape = (n_itr,) + a_host.shape
+    device_alloc = dpnp.empty(batch_shape, usm_type="device", sycl_queue=q)
+
+    e_a = None
+    e_b = None
+
+    for offset in range(n_itr):
+        _a = device_alloc[offset]
+        _a_data = dpnp.get_usm_ndarray(_a).usm_data
+
+        e_a = q.memcpy_async(
+            _a_data,
+            usm_host_data,
+            usm_host_data.nbytes,
+            [e_a] if e_a is not None else [],
+        )
+
+        e_a.wait()
+
+        _, e_a = dpex_exp.call_kernel_async(
+            async_kernel,
+            dpex.Range(len(_a)),
+            (e_a,),
+            _a,
+        )
+
+        e_a.wait()
+
+        e_a, e_b = e_b, e_a
+
+    q.wait()
+    dt = time.time() - t0
+
+    return dt, None, None
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Process some integers.")
+    parser.add_argument(
+        "--n",
+        type=int,
+        default=2_000_000,
+        help="an integer for the input array",
+    )
+    parser.add_argument(
+        "--n_itr", type=int, default=100, help="number of iterations"
+    )
+    parser.add_argument("--reps", type=int, default=5, help="number of repeats")
+    parser.add_argument(
+        "--algo",
+        type=str,
+        default="pipeline",
+        choices=["pipeline", "serial"],
+        help="algo",
+    )
+
+    args = parser.parse_args()
+
+    print(
+        "timing %d elements for %d iterations" % (args.n, args.n_itr),
+        flush=True,
+    )
+
+    print("using %f MB of memory" % (args.n * 4 / 1024 / 1024), flush=True)
+
+    a = np.arange(args.n, dtype=np.float32)
+
+    algo_func = {
+        "pipeline": run_pipeline,
+        "serial": run_serial,
+    }.get(args.algo)
+
+    for _ in range(args.reps):
+        dtp = algo_func(a, args.n_itr)
+        print(f"{args.algo} time tot|pci|cmp|speedup: {dtp}", flush=True)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/numba_dpex/examples/kernel/scan.py b/numba_dpex/examples/kernel/scan.py
index cd3906f771..bf2d194414 100644
--- a/numba_dpex/examples/kernel/scan.py
+++ b/numba_dpex/examples/kernel/scan.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/sum_reduction_ocl.py b/numba_dpex/examples/kernel/sum_reduction_ocl.py
index 5cf5a8d31f..889fca0082 100644
--- a/numba_dpex/examples/kernel/sum_reduction_ocl.py
+++ b/numba_dpex/examples/kernel/sum_reduction_ocl.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/sum_reduction_recursive_ocl.py b/numba_dpex/examples/kernel/sum_reduction_recursive_ocl.py
index 5e0553c98e..8cd4463464 100644
--- a/numba_dpex/examples/kernel/sum_reduction_recursive_ocl.py
+++ b/numba_dpex/examples/kernel/sum_reduction_recursive_ocl.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/vector_sum.py b/numba_dpex/examples/kernel/vector_sum.py
index c38657dce6..6b89cbe112 100644
--- a/numba_dpex/examples/kernel/vector_sum.py
+++ b/numba_dpex/examples/kernel/vector_sum.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/kernel/vector_sum2D.py b/numba_dpex/examples/kernel/vector_sum2D.py
index 09370cc360..0dd60d1165 100644
--- a/numba_dpex/examples/kernel/vector_sum2D.py
+++ b/numba_dpex/examples/kernel/vector_sum2D.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/sum_reduction.py b/numba_dpex/examples/sum_reduction.py
index d8a9dcaf44..2c7e66423b 100644
--- a/numba_dpex/examples/sum_reduction.py
+++ b/numba_dpex/examples/sum_reduction.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/examples/vectorize.py b/numba_dpex/examples/vectorize.py
index dc809fb774..6cc3aa8db9 100644
--- a/numba_dpex/examples/vectorize.py
+++ b/numba_dpex/examples/vectorize.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/experimental/__init__.py b/numba_dpex/experimental/__init__.py
new file mode 100644
index 0000000000..b860e215a6
--- /dev/null
+++ b/numba_dpex/experimental/__init__.py
@@ -0,0 +1,38 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Contains experimental features that are meant as engineering preview and not
+yet production ready.
+"""
+
+from numba.core.imputils import Registry
+
+from ._kernel_dpcpp_spirv_overloads import _atomic_ref_overloads
+from .decorators import device_func, kernel
+from .kernel_dispatcher import KernelDispatcher
+from .launcher import call_kernel, call_kernel_async
+from .literal_intenum_type import IntEnumLiteral
+from .models import *
+from .types import KernelDispatcherType
+
+registry = Registry()
+lower_constant = registry.lower_constant
+
+
+@lower_constant(KernelDispatcherType)
+def dpex_dispatcher_const(context):
+    """Dummy lowerer for a KernelDispatcherType object. It is added so that a
+    KernelDispatcher can be passed as an argument to dpjit.
+    """
+    return context.get_dummy_value()
+
+
+__all__ = [
+    "device_func",
+    "kernel",
+    "call_kernel",
+    "call_kernel_async",
+    "IntEnumLiteral",
+    "KernelDispatcher",
+]
diff --git a/numba_dpex/experimental/_kernel_dpcpp_spirv_overloads/__init__.py b/numba_dpex/experimental/_kernel_dpcpp_spirv_overloads/__init__.py
new file mode 100644
index 0000000000..1d329c9611
--- /dev/null
+++ b/numba_dpex/experimental/_kernel_dpcpp_spirv_overloads/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/experimental/_kernel_dpcpp_spirv_overloads/_atomic_ref_overloads.py b/numba_dpex/experimental/_kernel_dpcpp_spirv_overloads/_atomic_ref_overloads.py
new file mode 100644
index 0000000000..c6074c5749
--- /dev/null
+++ b/numba_dpex/experimental/_kernel_dpcpp_spirv_overloads/_atomic_ref_overloads.py
@@ -0,0 +1,518 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Implements the SPIR-V overloads for the kernel_api.atomic_ref class methods.
+"""
+
+from llvmlite import ir as llvmir
+from numba import errors
+from numba.core import cgutils, types
+from numba.extending import intrinsic, overload, overload_method
+
+from numba_dpex.core import itanium_mangler as ext_itanium_mangler
+from numba_dpex.core.targets.kernel_target import CC_SPIR_FUNC, LLVM_SPIRV_ARGS
+from numba_dpex.core.types import USMNdArray
+from numba_dpex.experimental.flag_enum import FlagEnum
+
+from ..dpcpp_types import AtomicRefType
+from ..kernel_iface import AddressSpace, AtomicRef, MemoryOrder, MemoryScope
+from ..target import DPEX_KERNEL_EXP_TARGET_NAME
+from ._spv_atomic_inst_helper import (
+    get_atomic_inst_name,
+    get_memory_semantics_mask,
+    get_scope,
+)
+
+
+def _parse_enum_or_int_literal_(literal_int) -> int:
+    """Parse an instance of an enum class or numba.core.types.Literal to its
+    actual int value.
+
+    Returns the Python int value for the numba Literal type.
+
+    Args:
+        literal_int: Instance of IntegerLiteral wrapping a Python int scalar
+        value
+    Raises:
+        TypingError: If the literal_int is not an IntegerLiteral type.
+    Returns:
+        int: The Python int value extracted from the literal_int.
+    """
+
+    if isinstance(literal_int, types.IntegerLiteral):
+        return literal_int.literal_value
+
+    if isinstance(literal_int, FlagEnum):
+        return literal_int.value
+
+    raise errors.TypingError("Could not parse input as an IntegerLiteral")
+
+
+def _intrinsic_helper(
+    ty_context, ty_atomic_ref, ty_val, op_str  # pylint: disable=unused-argument
+):
+    sig = types.void(ty_atomic_ref, ty_val)
+
+    def gen(context, builder, sig, args):
+        atomic_ref_ty = sig.args[0]
+        atomic_ref_dtype = atomic_ref_ty.dtype
+        retty = context.get_value_type(atomic_ref_dtype)
+
+        data_attr_pos = context.data_model_manager.lookup(
+            atomic_ref_ty
+        ).get_field_position("ref")
+
+        # TODO: evaluating the llvm-spirv flags that dpcpp uses
+        context.extra_compile_options[LLVM_SPIRV_ARGS] = [
+            "--spirv-ext=+SPV_EXT_shader_atomic_float_add"
+        ]
+
+        context.extra_compile_options[LLVM_SPIRV_ARGS] = [
+            "--spirv-ext=+SPV_EXT_shader_atomic_float_min_max"
+        ]
+
+        ptr_type = retty.as_pointer()
+        ptr_type.addrspace = atomic_ref_ty.address_space
+
+        spirv_fn_arg_types = [
+            ptr_type,
+            llvmir.IntType(32),
+            llvmir.IntType(32),
+            retty,
+        ]
+
+        mangled_fn_name = ext_itanium_mangler.mangle_ext(
+            get_atomic_inst_name(op_str, atomic_ref_dtype),
+            [
+                types.CPointer(
+                    atomic_ref_dtype, addrspace=atomic_ref_ty.address_space
+                ),
+                "__spv.Scope.Flag",
+                "__spv.MemorySemanticsMask.Flag",
+                atomic_ref_dtype,
+            ],
+        )
+        func = cgutils.get_or_insert_function(
+            builder.module,
+            llvmir.FunctionType(retty, spirv_fn_arg_types),
+            mangled_fn_name,
+        )
+        func.calling_convention = CC_SPIR_FUNC
+        spirv_memory_semantics_mask = get_memory_semantics_mask(
+            atomic_ref_ty.memory_order
+        )
+        spirv_scope = get_scope(atomic_ref_ty.memory_scope)
+
+        fn_args = [
+            builder.extract_value(args[0], data_attr_pos),
+            context.get_constant(types.int32, spirv_scope),
+            context.get_constant(types.int32, spirv_memory_semantics_mask),
+            args[1],
+        ]
+
+        builder.call(func, fn_args)
+
+    return sig, gen
+
+
+@intrinsic(target=DPEX_KERNEL_EXP_TARGET_NAME)
+def _intrinsic_fetch_add(ty_context, ty_atomic_ref, ty_val):
+    return _intrinsic_helper(ty_context, ty_atomic_ref, ty_val, "fetch_add")
+
+
+def _atomic_sub_float_wrapper(gen_fn):
+    def gen(context, builder, sig, args):
+        # args is a tuple, which is immutable
+        # covert tuple to list obj first before replacing arg[1]
+        # with fneg and convert back to tuple again.
+        args_lst = list(args)
+        args_lst[1] = builder.fneg(args[1])
+        args = tuple(args_lst)
+
+        gen_fn(context, builder, sig, args)
+
+    return gen
+
+
+@intrinsic(target=DPEX_KERNEL_EXP_TARGET_NAME)
+def _intrinsic_fetch_sub(ty_context, ty_atomic_ref, ty_val):
+    if ty_atomic_ref.dtype in (types.float32, types.float64):
+        # dpcpp does not support ``__spirv_AtomicFSubEXT``. fetch_sub
+        # for floats is implemented by negating the value and calling fetch_add.
+        # For example, A.fetch_sub(A, val) is implemented as A.fetch_add(-val).
+        sig, gen = _intrinsic_helper(
+            ty_context, ty_atomic_ref, ty_val, "fetch_add"
+        )
+        return sig, _atomic_sub_float_wrapper(gen)
+
+    return _intrinsic_helper(ty_context, ty_atomic_ref, ty_val, "fetch_sub")
+
+
+@intrinsic(target=DPEX_KERNEL_EXP_TARGET_NAME)
+def _intrinsic_fetch_min(ty_context, ty_atomic_ref, ty_val):
+    return _intrinsic_helper(ty_context, ty_atomic_ref, ty_val, "fetch_min")
+
+
+@intrinsic(target=DPEX_KERNEL_EXP_TARGET_NAME)
+def _intrinsic_fetch_max(ty_context, ty_atomic_ref, ty_val):
+    return _intrinsic_helper(ty_context, ty_atomic_ref, ty_val, "fetch_max")
+
+
+@intrinsic(target=DPEX_KERNEL_EXP_TARGET_NAME)
+def _intrinsic_fetch_and(ty_context, ty_atomic_ref, ty_val):
+    return _intrinsic_helper(ty_context, ty_atomic_ref, ty_val, "fetch_and")
+
+
+@intrinsic(target=DPEX_KERNEL_EXP_TARGET_NAME)
+def _intrinsic_fetch_or(ty_context, ty_atomic_ref, ty_val):
+    return _intrinsic_helper(ty_context, ty_atomic_ref, ty_val, "fetch_or")
+
+
+@intrinsic(target=DPEX_KERNEL_EXP_TARGET_NAME)
+def _intrinsic_fetch_xor(ty_context, ty_atomic_ref, ty_val):
+    return _intrinsic_helper(ty_context, ty_atomic_ref, ty_val, "fetch_xor")
+
+
+@intrinsic(target=DPEX_KERNEL_EXP_TARGET_NAME)
+def _intrinsic_atomic_ref_ctor(
+    ty_context, ref, ty_index, ty_retty_ref  # pylint: disable=unused-argument
+):
+    ty_retty = ty_retty_ref.instance_type
+    sig = ty_retty(ref, ty_index, ty_retty_ref)
+
+    def codegen(context, builder, sig, args):
+        ref = args[0]
+        index_pos = args[1]
+
+        dmm = context.data_model_manager
+        data_attr_pos = dmm.lookup(sig.args[0]).get_field_position("data")
+        data_attr = builder.extract_value(ref, data_attr_pos)
+
+        with builder.goto_entry_block():
+            ptr_to_data_attr = builder.alloca(data_attr.type)
+        builder.store(data_attr, ptr_to_data_attr)
+        ref_ptr_value = builder.gep(builder.load(ptr_to_data_attr), [index_pos])
+
+        atomic_ref_struct = cgutils.create_struct_proxy(ty_retty)(
+            context, builder
+        )
+        ref_attr_pos = dmm.lookup(ty_retty).get_field_position("ref")
+        atomic_ref_struct[ref_attr_pos] = ref_ptr_value
+        # pylint: disable=protected-access
+        return atomic_ref_struct._getvalue()
+
+    return (
+        sig,
+        codegen,
+    )
+
+
+def _check_if_supported_ref(ref):
+    supported = True
+
+    if not isinstance(ref, USMNdArray):
+        raise errors.TypingError(
+            f"Cannot create an AtomicRef from {ref}. "
+            "An AtomicRef can only be constructed from a 0-dimensional "
+            "dpctl.tensor.usm_ndarray or a dpnp.ndarray array."
+        )
+    if ref.dtype not in [
+        types.int32,
+        types.uint32,
+        types.float32,
+        types.int64,
+        types.uint64,
+        types.float64,
+    ]:
+        raise errors.TypingError(
+            "Cannot create an AtomicRef from a tensor with an unsupported "
+            "element type."
+        )
+    if ref.dtype in [types.int64, types.float64, types.uint64]:
+        if not ref.queue.device_has_aspect_atomic64:
+            raise errors.TypingError(
+                "Targeted device does not support 64-bit atomic operations."
+            )
+
+    return supported
+
+
+@overload(
+    AtomicRef,
+    prefer_literal=True,
+    target=DPEX_KERNEL_EXP_TARGET_NAME,
+)
+def ol_atomic_ref(
+    ref,
+    index=0,
+    memory_order=MemoryOrder.RELAXED,
+    memory_scope=MemoryScope.DEVICE,
+    address_space=AddressSpace.GLOBAL,
+):
+    """Overload of the constructor for the class
+    class:`numba_dpex.experimental.kernel_iface.AtomicRef`.
+
+    Raises:
+        errors.TypingError: If the `ref` argument is not a UsmNdArray type.
+        errors.TypingError: If the dtype of the `ref` is not supported in an
+        AtomicRef.
+        errors.TypingError: If the device does not support atomic operations on
+        the dtype of the `ref`.
+        errors.TypingError: If the `memory_order`, `address_type`, or
+        `memory_scope` arguments could not be parsed as integer literals.
+        errors.TypingError: If the `address_space` argument is different from
+        the address space attribute of the `ref` argument.
+        errors.TypingError: If the address space is PRIVATE.
+
+    """
+    _check_if_supported_ref(ref)
+
+    try:
+        _address_space = _parse_enum_or_int_literal_(address_space)
+    except errors.TypingError as exc:
+        raise errors.TypingError(
+            "Address space argument to AtomicRef constructor should "
+            "be an IntegerLiteral."
+        ) from exc
+
+    try:
+        _memory_order = _parse_enum_or_int_literal_(memory_order)
+    except errors.TypingError as exc:
+        raise errors.TypingError(
+            "Memory order argument to AtomicRef constructor should "
+            "be an IntegerLiteral."
+        ) from exc
+
+    try:
+        _memory_scope = _parse_enum_or_int_literal_(memory_scope)
+    except errors.TypingError as exc:
+        raise errors.TypingError(
+            "Memory scope argument to AtomicRef constructor should "
+            "be an IntegerLiteral."
+        ) from exc
+
+    if _address_space != ref.addrspace:
+        raise errors.TypingError(
+            "The address_space specified via the AtomicRef constructor "
+            f"{_address_space} does not match the address space "
+            f"{ref.addrspace} of the referred object for which the AtomicRef "
+            "is to be constructed."
+        )
+
+    if _address_space == AddressSpace.PRIVATE:
+        raise errors.TypingError(
+            f"Unsupported address space value {_address_space}. Supported "
+            f"address space values are: Generic ({AddressSpace.GENERIC}), "
+            f"Global ({AddressSpace.GLOBAL}), and Local ({AddressSpace.LOCAL})."
+        )
+
+    ty_retty = AtomicRefType(
+        dtype=ref.dtype,
+        memory_order=_memory_order,
+        memory_scope=_memory_scope,
+        address_space=_address_space,
+    )
+
+    def ol_atomic_ref_ctor_impl(
+        ref,
+        index=0,
+        memory_order=MemoryOrder.RELAXED,  # pylint: disable=unused-argument
+        memory_scope=MemoryScope.DEVICE,  # pylint: disable=unused-argument
+        address_space=AddressSpace.GLOBAL,  # pylint: disable=unused-argument
+    ):
+        # pylint: disable=no-value-for-parameter
+        return _intrinsic_atomic_ref_ctor(ref, index, ty_retty)
+
+    return ol_atomic_ref_ctor_impl
+
+
+@overload_method(AtomicRefType, "fetch_add", target=DPEX_KERNEL_EXP_TARGET_NAME)
+def ol_fetch_add(atomic_ref, val):
+    """SPIR-V overload for
+    :meth:`numba_dpex.experimental.kernel_iface.AtomicRef.fetch_add`.
+
+    Generates the same LLVM IR instruction as dpcpp for the
+    `atomic_ref::fetch_add` function.
+
+    Raises:
+        TypingError: When the dtype of the aggregator value does not match the
+        dtype of the AtomicRef type.
+    """
+    if atomic_ref.dtype != val:
+        raise errors.TypingError(
+            f"Type of value to add: {val} does not match the type of the "
+            f"reference: {atomic_ref.dtype} stored in the atomic ref."
+        )
+
+    def ol_fetch_add_impl(atomic_ref, val):
+        # pylint: disable=no-value-for-parameter
+        return _intrinsic_fetch_add(atomic_ref, val)
+
+    return ol_fetch_add_impl
+
+
+@overload_method(AtomicRefType, "fetch_sub", target=DPEX_KERNEL_EXP_TARGET_NAME)
+def ol_fetch_sub(atomic_ref, val):
+    """SPIR-V overload for
+    :meth:`numba_dpex.experimental.kernel_iface.AtomicRef.fetch_sub`.
+
+    Generates the same LLVM IR instruction as dpcpp for the
+    `atomic_ref::fetch_sub` function.
+
+    Raises:
+        TypingError: When the dtype of the aggregator value does not match the
+        dtype of the AtomicRef type.
+    """
+    if atomic_ref.dtype != val:
+        raise errors.TypingError(
+            f"Type of value to sub: {val} does not match the type of the "
+            f"reference: {atomic_ref.dtype} stored in the atomic ref."
+        )
+
+    def ol_fetch_sub_impl(atomic_ref, val):
+        # pylint: disable=no-value-for-parameter
+        return _intrinsic_fetch_sub(atomic_ref, val)
+
+    return ol_fetch_sub_impl
+
+
+@overload_method(AtomicRefType, "fetch_min", target=DPEX_KERNEL_EXP_TARGET_NAME)
+def ol_fetch_min(atomic_ref, val):
+    """SPIR-V overload for
+    :meth:`numba_dpex.experimental.kernel_iface.AtomicRef.fetch_min`.
+
+    Generates the same LLVM IR instruction as dpcpp for the
+    `atomic_ref::fetch_min` function.
+
+    Raises:
+        TypingError: When the dtype of the aggregator value does not match the
+        dtype of the AtomicRef type.
+    """
+    if atomic_ref.dtype != val:
+        raise errors.TypingError(
+            f"Type of value to find min: {val} does not match the type of the "
+            f"reference: {atomic_ref.dtype} stored in the atomic ref."
+        )
+
+    def ol_fetch_min_impl(atomic_ref, val):
+        # pylint: disable=no-value-for-parameter
+        return _intrinsic_fetch_min(atomic_ref, val)
+
+    return ol_fetch_min_impl
+
+
+@overload_method(AtomicRefType, "fetch_max", target=DPEX_KERNEL_EXP_TARGET_NAME)
+def ol_fetch_max(atomic_ref, val):
+    """SPIR-V overload for
+    :meth:`numba_dpex.experimental.kernel_iface.AtomicRef.fetch_max`.
+
+    Generates the same LLVM IR instruction as dpcpp for the
+    `atomic_ref::fetch_max` function.
+
+    Raises:
+        TypingError: When the dtype of the aggregator value does not match the
+        dtype of the AtomicRef type.
+    """
+    if atomic_ref.dtype != val:
+        raise errors.TypingError(
+            f"Type of value to find max: {val} does not match the type of the "
+            f"reference: {atomic_ref.dtype} stored in the atomic ref."
+        )
+
+    def ol_fetch_max_impl(atomic_ref, val):
+        # pylint: disable=no-value-for-parameter
+        return _intrinsic_fetch_max(atomic_ref, val)
+
+    return ol_fetch_max_impl
+
+
+@overload_method(AtomicRefType, "fetch_and", target=DPEX_KERNEL_EXP_TARGET_NAME)
+def ol_fetch_and(atomic_ref, val):
+    """SPIR-V overload for
+    :meth:`numba_dpex.experimental.kernel_iface.AtomicRef.fetch_and`.
+
+    Generates the same LLVM IR instruction as dpcpp for the
+    `atomic_ref::fetch_and` function.
+
+    Raises:
+        TypingError: When the dtype of the aggregator value does not match the
+        dtype of the AtomicRef type.
+    """
+    if atomic_ref.dtype != val:
+        raise errors.TypingError(
+            f"Type of value to and: {val} does not match the type of the "
+            f"reference: {atomic_ref.dtype} stored in the atomic ref."
+        )
+
+    if atomic_ref.dtype not in (types.int32, types.int64):
+        raise errors.TypingError(
+            "fetch_and operation only supported on int32 and int64 dtypes."
+        )
+
+    def ol_fetch_and_impl(atomic_ref, val):
+        # pylint: disable=no-value-for-parameter
+        return _intrinsic_fetch_and(atomic_ref, val)
+
+    return ol_fetch_and_impl
+
+
+@overload_method(AtomicRefType, "fetch_or", target=DPEX_KERNEL_EXP_TARGET_NAME)
+def ol_fetch_or(atomic_ref, val):
+    """SPIR-V overload for
+    :meth:`numba_dpex.experimental.kernel_iface.AtomicRef.fetch_or`.
+
+    Generates the same LLVM IR instruction as dpcpp for the
+    `atomic_ref::fetch_or` function.
+
+    Raises:
+        TypingError: When the dtype of the aggregator value does not match the
+        dtype of the AtomicRef type.
+    """
+    if atomic_ref.dtype != val:
+        raise errors.TypingError(
+            f"Type of value to or: {val} does not match the type of the "
+            f"reference: {atomic_ref.dtype} stored in the atomic ref."
+        )
+
+    if atomic_ref.dtype not in (types.int32, types.int64):
+        raise errors.TypingError(
+            "fetch_or operation only supported on int32 and int64 dtypes."
+        )
+
+    def ol_fetch_or_impl(atomic_ref, val):
+        # pylint: disable=no-value-for-parameter
+        return _intrinsic_fetch_or(atomic_ref, val)
+
+    return ol_fetch_or_impl
+
+
+@overload_method(AtomicRefType, "fetch_xor", target=DPEX_KERNEL_EXP_TARGET_NAME)
+def ol_fetch_xor(atomic_ref, val):
+    """SPIR-V overload for
+    :meth:`numba_dpex.experimental.kernel_iface.AtomicRef.fetch_xor`.
+
+    Generates the same LLVM IR instruction as dpcpp for the
+    `atomic_ref::fetch_xor` function.
+
+    Raises:
+        TypingError: When the dtype of the aggregator value does not match the
+        dtype of the AtomicRef type.
+    """
+    if atomic_ref.dtype != val:
+        raise errors.TypingError(
+            f"Type of value to xor: {val} does not match the type of the "
+            f"reference: {atomic_ref.dtype} stored in the atomic ref."
+        )
+
+    if atomic_ref.dtype not in (types.int32, types.int64):
+        raise errors.TypingError(
+            "fetch_xor operation only supported on int32 and int64 dtypes."
+        )
+
+    def ol_fetch_xor_impl(atomic_ref, val):
+        # pylint: disable=no-value-for-parameter
+        return _intrinsic_fetch_xor(atomic_ref, val)
+
+    return ol_fetch_xor_impl
diff --git a/numba_dpex/experimental/_kernel_dpcpp_spirv_overloads/_spv_atomic_inst_helper.py b/numba_dpex/experimental/_kernel_dpcpp_spirv_overloads/_spv_atomic_inst_helper.py
new file mode 100644
index 0000000000..948589b581
--- /dev/null
+++ b/numba_dpex/experimental/_kernel_dpcpp_spirv_overloads/_spv_atomic_inst_helper.py
@@ -0,0 +1,170 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Helper module to generate LLVM IR SPIR-V intrinsic calls.
+"""
+from enum import IntEnum
+
+from numba.core import types
+
+from ..kernel_iface import MemoryOrder, MemoryScope
+
+
+class _SpvScope(IntEnum):
+    """
+    An enumeration to store the dpcpp values for SPIR-V memory scope.
+    """
+
+    CROSS_DEVICE = 0
+    DEVICE = 1
+    WORKGROUP = 2
+    SUBGROUP = 3
+    INVOCATION = 4
+
+
+class _SpvMemorySemanticsMask(IntEnum):
+    """
+    An enumeration to store the dpcpp values for SPIR-V mask.
+
+    """
+
+    NONE = 0x0
+    ACQUIRE = 0x2
+    RELEASE = 0x4
+    ACQUIRE_RELEASE = 0x8
+    SEQUENTIALLY_CONSISTENT = 0x10
+    UNIFORM_MEMORY = 0x40
+    SUBGROUP_MEMORY = 0x80
+    WORKGROUP_MEMORY = 0x100
+    CROSS_WORKGROUP_MEMORY = 0x200
+    ATOMIC_COUNTER_MEMORY = 0x400
+    IMAGE_MEMORY = 0x800
+
+
+_spv_atomic_instructions_map = {
+    "fetch_add": {
+        types.int32: "__spirv_AtomicIAdd",
+        types.int64: "__spirv_AtomicIAdd",
+        types.float32: "__spirv_AtomicFAddEXT",
+        types.float64: "__spirv_AtomicFAddEXT",
+    },
+    "fetch_sub": {
+        types.int32: "__spirv_AtomicISub",
+        types.int64: "__spirv_AtomicISub",
+        types.float32: "__spirv_AtomicFSubEXT",
+        types.float64: "__spirv_AtomicFSubEXT",
+    },
+    "fetch_min": {
+        types.int32: "__spirv_AtomicSMin",
+        types.int64: "__spirv_AtomicSMin",
+        types.float32: "__spirv_AtomicFMinEXT",
+        types.float64: "__spirv_AtomicFMinEXT",
+    },
+    "fetch_max": {
+        types.int32: "__spirv_AtomicSMax",
+        types.int64: "__spirv_AtomicSMax",
+        types.float32: "__spirv_AtomicFMaxEXT",
+        types.float64: "__spirv_AtomicFMaxEXT",
+    },
+    "fetch_and": {
+        types.int32: "__spirv_AtomicAnd",
+        types.int64: "__spirv_AtomicAnd",
+    },
+    "fetch_or": {
+        types.int32: "__spirv_AtomicOr",
+        types.int64: "__spirv_AtomicOr",
+    },
+    "fetch_xor": {
+        types.int32: "__spirv_AtomicXor",
+        types.int64: "__spirv_AtomicXor",
+    },
+}
+
+
+def get_atomic_inst_name(atomic_inst: str, atomic_ref_dtype: types.Type) -> str:
+    """Returns a string corresponding to the LLVM IR intrinsic function
+    generated for a particular atomic operation for a specific data type.
+
+    Args:
+        atomic_inst (str): The string name of an atomic operation to look up.
+        atomic_ref_dtype (numba.core.types.Type): The Numba type object
+        corresponding to the dtype of the object stored in an AtomicRef.
+
+    Raises:
+        ValueError: If the atomic operation is not found in the
+        ``_spv_atomic_instructions_map`` dictionary.
+        ValueError: If no instruction is found for a combination of the
+        atomic operation and the dtype in the ``_spv_atomic_instructions_map``
+        dictionary.
+
+    Returns:
+        str: A string corresponding to the LLVM IR intrinsic that should be
+        generated for the atomic operation for the specified dtype.
+    """
+    inst_ref_types_map = _spv_atomic_instructions_map.get(atomic_inst)
+    if inst_ref_types_map is None:
+        raise ValueError("Unsupported atomic instruction")
+
+    inst_name = inst_ref_types_map.get(atomic_ref_dtype)
+
+    if inst_name is None:
+        raise ValueError(
+            "Unsupported atomic reference type for instruction " + atomic_inst
+        )
+    return inst_name
+
+
+def get_memory_semantics_mask(memory_order):
+    """
+    Equivalent of the getMemorySemanticsMask function in dpcpp's
+    sycl/include/sycl/detail/spirv.hpp.  Translates SYCL memory order to a
+    SPIR-V  memory semantics mask.
+
+    """
+
+    spv_order = _SpvMemorySemanticsMask.NONE.value
+
+    if memory_order == MemoryOrder.RELAXED.value:
+        spv_order = _SpvMemorySemanticsMask.NONE.value
+    elif memory_order == MemoryOrder.CONSUME_UNSUPPORTED.value:
+        pass
+    elif memory_order == MemoryOrder.ACQUIRE.value:
+        spv_order = _SpvMemorySemanticsMask.ACQUIRE.value
+    elif memory_order == MemoryOrder.RELEASE.value:
+        spv_order = _SpvMemorySemanticsMask.RELEASE.value
+    elif memory_order == MemoryOrder.ACQ_REL.value:
+        spv_order = _SpvMemorySemanticsMask.ACQUIRE_RELEASE.value
+    elif memory_order == MemoryOrder.SEQ_CST.value:
+        spv_order = _SpvMemorySemanticsMask.SEQUENTIALLY_CONSISTENT.value
+    else:
+        raise ValueError("Invalid memory order provided")
+
+    return (
+        spv_order
+        | _SpvMemorySemanticsMask.SUBGROUP_MEMORY.value
+        | _SpvMemorySemanticsMask.WORKGROUP_MEMORY.value
+        | _SpvMemorySemanticsMask.CROSS_WORKGROUP_MEMORY.value
+    )
+
+
+def get_scope(memory_scope):
+    """
+    Translates a memory scope enum value to an equivalent SPIR-V memory scope
+    value.
+    """
+    retval = None
+    if memory_scope == MemoryScope.WORK_ITEM.value:
+        retval = _SpvScope.INVOCATION.value
+    elif memory_scope == MemoryScope.SUB_GROUP.value:
+        retval = _SpvScope.SUBGROUP.value
+    elif memory_scope == MemoryScope.WORK_GROUP.value:
+        retval = _SpvScope.WORKGROUP.value
+    elif memory_scope == MemoryScope.DEVICE.value:
+        retval = _SpvScope.DEVICE.value
+    elif memory_scope == MemoryScope.SYSTEM.value:
+        retval = _SpvScope.CROSS_DEVICE.value
+    else:
+        raise ValueError("Invalid memory scope provided")
+
+    return retval
diff --git a/numba_dpex/experimental/decorators.py b/numba_dpex/experimental/decorators.py
new file mode 100644
index 0000000000..df44320572
--- /dev/null
+++ b/numba_dpex/experimental/decorators.py
@@ -0,0 +1,147 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+""" The set of experimental decorators provided by numba_dpex that are not yet
+ready to move to numba_dpex.core.
+"""
+import inspect
+from warnings import warn
+
+from numba.core import sigutils, typeinfer
+from numba.core.target_extension import (
+    jit_registry,
+    resolve_dispatcher_from_str,
+    target_registry,
+)
+
+from numba_dpex.core.targets.kernel_target import CompilationMode
+from numba_dpex.experimental.kernel_dispatcher import KernelDispatcher
+
+from .target import DPEX_KERNEL_EXP_TARGET_NAME
+
+
+def _parse_func_or_sig(signature_or_function):
+    # Handle signature (borrowed from numba). swapped signature and list check
+    if signature_or_function is None:
+        # No signature, no function
+        pyfunc = None
+        sigs = []
+    elif sigutils.is_signature(signature_or_function):
+        # A single signature is passed
+        pyfunc = None
+        sigs = [signature_or_function]
+    elif isinstance(signature_or_function, list):
+        # A list of signatures is passed
+        pyfunc = None
+        sigs = signature_or_function
+    else:
+        # A function is passed
+        pyfunc = signature_or_function
+        sigs = []
+
+    return pyfunc, sigs
+
+
+def kernel(func_or_sig=None, **options):
+    """A decorator to define a kernel function.
+
+    A kernel function is conceptually equivalent to a SYCL kernel function, and
+    gets compiled into either an OpenCL or a LevelZero SPIR-V binary kernel.
+    A kernel decorated Python function has the following restrictions:
+
+        * The function can not return any value.
+        * All array arguments passed to a kernel should adhere to compute
+          follows data programming model.
+    """
+
+    # dispatcher is a type:
+    # <class 'numba_dpex.experimental.kernel_dispatcher.KernelDispatcher'>
+    dispatcher = resolve_dispatcher_from_str(DPEX_KERNEL_EXP_TARGET_NAME)
+    if "_compilation_mode" in options:
+        user_compilation_mode = options["_compilation_mode"]
+        warn(
+            "_compilation_mode is an internal flag that should not be set "
+            "in the decorator. The decorator defined option "
+            f"{user_compilation_mode} is going to be ignored."
+        )
+    options["_compilation_mode"] = CompilationMode.KERNEL
+
+    # FIXME: The options need to be evaluated and checked here like it is
+    # done in numba.core.decorators.jit
+
+    func, sigs = _parse_func_or_sig(func_or_sig)
+    for sig in sigs:
+        if isinstance(sig, str):
+            raise NotImplementedError(
+                "Specifying signatures as string is not yet supported by numba-dpex"
+            )
+
+    def _kernel_dispatcher(pyfunc):
+        disp: KernelDispatcher = dispatcher(
+            pyfunc=pyfunc,
+            targetoptions=options,
+        )
+
+        if len(sigs) > 0:
+            with typeinfer.register_dispatcher(disp):
+                for sig in sigs:
+                    disp.compile(sig)
+                disp.disable_compile()
+
+        return disp
+
+    if func is None:
+        return _kernel_dispatcher
+
+    if not inspect.isfunction(func):
+        raise ValueError(
+            "Argument passed to the kernel decorator is neither a "
+            "function object, nor a signature. If you are trying to "
+            "specialize the kernel that takes a single argument, specify "
+            "the return type as void explicitly."
+        )
+    return _kernel_dispatcher(func)
+
+
+def device_func(func_or_sig=None, **options):
+    """Generates a function with a device-only calling convention, e.g.,
+    spir_func for SPIR-V based devices.
+
+    The decorator is used to compile overloads in the DpexKernelTarget and
+    users should use the decorator to define functions that are only callable
+    from inside another device_func or a kernel.
+
+    A device_func is not compiled down to device binary IR and instead left as
+    LLVM IR. It is done so that the function can be inlined fully into the
+    kernel module from where it is used at the LLVM level, leading to more
+    optimization opportunities.
+
+    Returns:
+        KernelDispatcher: A KernelDispatcher instance with the
+        _compilation_mode option set to DEVICE_FUNC.
+    """
+    dispatcher = resolve_dispatcher_from_str(DPEX_KERNEL_EXP_TARGET_NAME)
+
+    if "_compilation_mode" in options:
+        user_compilation_mode = options["_compilation_mode"]
+        warn(
+            "_compilation_mode is an internal flag that should not be set "
+            "in the decorator. The decorator defined option "
+            f"{user_compilation_mode} is going to be ignored."
+        )
+    options["_compilation_mode"] = CompilationMode.DEVICE_FUNC
+
+    def _kernel_dispatcher(pyfunc):
+        return dispatcher(
+            pyfunc=pyfunc,
+            targetoptions=options,
+        )
+
+    if func_or_sig is None:
+        return _kernel_dispatcher
+
+    return _kernel_dispatcher(func_or_sig)
+
+
+jit_registry[target_registry[DPEX_KERNEL_EXP_TARGET_NAME]] = device_func
diff --git a/numba_dpex/experimental/dpcpp_types.py b/numba_dpex/experimental/dpcpp_types.py
new file mode 100644
index 0000000000..6cc5961f24
--- /dev/null
+++ b/numba_dpex/experimental/dpcpp_types.py
@@ -0,0 +1,82 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Collection of numba-dpex typing classes for kernel_iface Python classes.
+"""
+
+from numba.core.types import Type
+
+
+class AtomicRefType(Type):
+    """numba-dpex internal type to represent a Python object of
+    :class:`numba_dpex.experimental.kernel_iface.AtomicRef`.
+    """
+
+    def __init__(
+        self,
+        dtype: Type,
+        memory_order: int,
+        memory_scope: int,
+        address_space: int,
+    ):
+        """Creates an instance of the AtomicRefType representing a Python
+        AtomicRef object.
+        """
+        self._dtype = dtype
+        self._memory_order = memory_order
+        self._memory_scope = memory_scope
+        self._address_space = address_space
+        name = (
+            f"AtomicRef< {self._dtype}, "
+            f"memory_order= {self._memory_order}, "
+            f"memory_scope= {self._memory_scope}, "
+            f"address_space= {self._address_space}>"
+        )
+        super().__init__(name=name)
+
+    @property
+    def memory_order(self) -> int:
+        """Returns the integer value for a memory order that corresponds to
+        kernel_iface.MemoryOrder.
+        """
+        return self._memory_order
+
+    @property
+    def memory_scope(self) -> int:
+        """Returns the integer value for a memory order that corresponds to
+        kernel_iface.MemoryScope.
+        """
+        return self._memory_scope
+
+    @property
+    def address_space(self) -> int:
+        """Returns the integer value for a memory order that corresponds to
+        kernel_iface.AddressSpace.
+        """
+        return self._address_space
+
+    @property
+    def dtype(self):
+        """Returns the Numba type of the object corresponding to the value
+        stored in an AtomicRef.
+        """
+        return self._dtype
+
+    @property
+    def key(self):
+        """
+        A property used for __eq__, __ne__ and __hash__.
+        """
+        return (
+            self.dtype,
+            self.memory_order,
+            self.memory_scope,
+            self.address_space,
+        )
+
+    def cast_python_value(self, args):
+        """The helper function is not overloaded and using it on the
+        AtomicRefType throws a NotImplementedError.
+        """
+        raise NotImplementedError
diff --git a/numba_dpex/experimental/flag_enum.py b/numba_dpex/experimental/flag_enum.py
new file mode 100644
index 0000000000..e800c0addf
--- /dev/null
+++ b/numba_dpex/experimental/flag_enum.py
@@ -0,0 +1,25 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Provides a FlagEnum class to help distinguish IntEnum types that numba-dpex
+intends to use as Integer literal types inside the compiler type inferring
+infrastructure.
+"""
+from enum import IntEnum
+
+
+class FlagEnum(IntEnum):
+    """Helper class to distinguish IntEnum types that numba-dpex should consider
+    as Numba Literal types.
+    """
+
+    @classmethod
+    def basetype(cls) -> int:
+        """Returns an dummy int object that helps numba-dpex infer the type of
+        an instance of a FlagEnum class.
+
+        Returns:
+            int: Dummy int value
+        """
+        return int(0)
diff --git a/numba_dpex/experimental/kernel_dispatcher.py b/numba_dpex/experimental/kernel_dispatcher.py
new file mode 100644
index 0000000000..13a539c970
--- /dev/null
+++ b/numba_dpex/experimental/kernel_dispatcher.py
@@ -0,0 +1,466 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Implements a new numba dispatcher class and a compiler class to compile and
+call numba_dpex.kernel decorated function.
+"""
+from collections import namedtuple
+from contextlib import ExitStack
+from typing import Tuple
+
+import numba.core.event as ev
+from llvmlite.binding.value import ValueRef
+from numba.core import errors, sigutils, types
+from numba.core.compiler import CompileResult, Flags
+from numba.core.compiler_lock import global_compiler_lock
+from numba.core.dispatcher import Dispatcher, _FunctionCompiler
+from numba.core.funcdesc import PythonFunctionDescriptor
+from numba.core.target_extension import (
+    dispatcher_registry,
+    target_override,
+    target_registry,
+)
+from numba.core.types import Array as NpArrayType
+from numba.core.types import void
+from numba.core.typing.typeof import Purpose, typeof
+
+from numba_dpex import config, numba_sem_version, spirv_generator
+from numba_dpex.core.codegen import SPIRVCodeLibrary
+from numba_dpex.core.exceptions import (
+    ExecutionQueueInferenceError,
+    InvalidKernelSpecializationError,
+    KernelHasReturnValueError,
+    UnsupportedKernelArgumentError,
+)
+from numba_dpex.core.pipelines import kernel_compiler
+from numba_dpex.core.targets.kernel_target import (
+    CompilationMode,
+    DpexKernelTargetContext,
+)
+from numba_dpex.core.types import USMNdArray
+from numba_dpex.core.utils import kernel_launcher as kl
+
+from .target import DPEX_KERNEL_EXP_TARGET_NAME, dpex_exp_kernel_target
+
+_KernelCompileResult = namedtuple(
+    "_KernelCompileResult", CompileResult._fields + ("kernel_device_ir_module",)
+)
+
+
+class _KernelCompiler(_FunctionCompiler):
+    """A special compiler class used to compile numba_dpex.kernel decorated
+    functions.
+    """
+
+    def check_queue_equivalence_of_args(
+        self, py_func_name: str, args: [types.Type, ...]
+    ):
+        """Evaluates if all USMNdArray arguments passed to a kernel function
+        has the same DpctlSyclQueue type.
+
+        Args:
+            py_func_name (str): Name of the kernel that is being evaluated
+            args (types.Type, ...]): List of numba inferred types for each
+            argument passed to the kernel
+
+        Raises:
+            ExecutionQueueInferenceError: If all USMNdArray were not allocated
+            on the same dpctl.SyclQueue
+            ExecutionQueueInferenceError: If there were not USMNdArray
+            arguments passed to the kernel.
+        """
+        common_queue = None
+
+        for arg in args:
+            if isinstance(arg, USMNdArray):
+                if common_queue is None:
+                    common_queue = arg.queue
+                elif common_queue != arg.queue:
+                    raise ExecutionQueueInferenceError(
+                        kernel_name=py_func_name, usmarray_argnum_list=[]
+                    )
+
+        if common_queue is None:
+            raise ExecutionQueueInferenceError(
+                kernel_name=py_func_name, usmarray_argnum_list=None
+            )
+
+    def check_sig_types(self, py_func_name: str, argtypes, return_type):
+        """Checks signature type and raises error if wrong signature was
+        provided.
+
+        Args:
+            sig: The signature on which the kernel is to be specialized.
+
+        Raises:
+            KernelHasReturnValueError: non void return type.
+            InvalidKernelSpecializationError: unsupported arguments where
+                provided.
+        """
+
+        if return_type is None:
+            return_type = void
+        sig = return_type(*argtypes)
+
+        # Check if signature has a non-void return type
+        if return_type and return_type != void:
+            raise KernelHasReturnValueError(
+                kernel_name=None,
+                return_type=return_type,
+                sig=sig,
+            )
+
+        # USMNdarray check
+        usmarray_argnums = []
+        usmndarray_argtypes = []
+        unsupported_argnum_list = []
+
+        for i, argtype in enumerate(argtypes):
+            # FIXME: Add checks for other types of unsupported kernel args, e.g.
+            # complex.
+
+            # Check if a non-USMNdArray Array type is passed to the kernel
+            if isinstance(argtype, NpArrayType) and not isinstance(
+                argtype, USMNdArray
+            ):
+                unsupported_argnum_list.append(i)
+            elif isinstance(argtype, USMNdArray):
+                usmarray_argnums.append(i)
+                usmndarray_argtypes.append(argtype)
+
+        if unsupported_argnum_list:
+            raise InvalidKernelSpecializationError(
+                kernel_name=py_func_name,
+                invalid_sig=sig,
+                unsupported_argnum_list=unsupported_argnum_list,
+            )
+
+    def check_arguments(self, py_func_name: str, args: [types.Type, ...]):
+        """Checks arguments and queue of the input arguments.
+
+        Raises:
+            KernelHasReturnValueError: non void return type.
+            InvalidKernelSpecializationError: unsupported arguments where
+                provided.
+            ExecutionQueueInferenceError: If all USMNdArray were not allocated
+                on the same dpctl.SyclQueue
+            ExecutionQueueInferenceError: If there were not USMNdArray
+                arguments passed to the kernel.
+        """
+        self.check_sig_types(py_func_name, args, None)
+        self.check_queue_equivalence_of_args(py_func_name, args)
+
+    def _compile_to_spirv(
+        self,
+        kernel_library: SPIRVCodeLibrary,
+        kernel_fndesc: PythonFunctionDescriptor,
+        kernel_targetctx: DpexKernelTargetContext,
+    ):
+        kernel_func: ValueRef = kernel_library.get_function(
+            kernel_fndesc.llvm_func_name
+        )
+
+        # Create a spir_kernel wrapper function
+        kernel_fn = kernel_targetctx.prepare_spir_kernel(
+            kernel_func, kernel_fndesc.argtypes
+        )
+        # Get the compiler flags that were passed through the target descriptor
+        flags = Flags()
+        self.targetdescr.options.parse_as_flags(flags, self.targetoptions)
+
+        # If the inline_threshold option was set then set the property in the
+        # kernel_library to force inlining ``overload`` calls into a kernel.
+        inline_threshold = flags.inline_threshold  # pylint: disable=E1101
+        kernel_library.inline_threshold = inline_threshold
+
+        # Call finalize on the LLVM module. Finalization will result in
+        # all linking libraries getting linked together and final optimization
+        # including inlining of functions if an inlining level is specified.
+        kernel_library.finalize()
+        # Compiled the LLVM IR to SPIR-V
+        kernel_spirv_module = spirv_generator.llvm_to_spirv(
+            kernel_targetctx,
+            kernel_library.final_module,
+            kernel_library.final_module.as_bitcode(),
+        )
+        return kl.SPIRVKernelModule(
+            kernel_name=kernel_fn.name, kernel_bitcode=kernel_spirv_module
+        )
+
+    def compile(self, args, return_type) -> _KernelCompileResult:
+        status, kcres = self._compile_cached(args, return_type)
+        if status:
+            return kcres
+
+        raise kcres
+
+    def _compile_cached(
+        self, args, return_type: types.Type
+    ) -> Tuple[bool, _KernelCompileResult]:
+        """Compiles the kernel function to bitcode and generates a host-callable
+        wrapper to submit the kernel to a SYCL queue.
+
+        The LLVM IR generated for the kernel function is available in the
+        CompileResult objected returned by
+        numba_dpex.core.pipeline.kernel_compiler.KernelCompiler.
+
+        Once the kernel decorated function is compiled down to LLVM IR, the
+        following steps are performed:
+
+            a) compile the IR into SPIR-V kernel
+            b) generate a host callable wrapper function that will create a
+               sycl::kernel_bundle from the SPIR-V and then submits the
+               kernel_bundle to a sycl::queue
+            c) create a cpython_wrapper_function for the host callable wrapper
+               function.
+            d) create a cfunc_wrapper_function to make the host callable wrapper
+               function callable inside another JIT-compiled function.
+
+        Args:
+            args (tuple(types.Type)): A tuple of numba.core.Type instances each
+            representing the numba-inferred type of a kernel argument.
+
+            return_type (types.Type): The numba-inferred type of the returned
+            value from the kernel. Should always be types.NoneType.
+
+        Returns:
+            CompileResult: A CompileResult object storing the LLVM library for
+            the host-callable wrapper function.
+        """
+        key = tuple(args), return_type
+        try:
+            return False, self._failed_cache[key]
+        except KeyError:
+            pass
+
+        try:
+            with target_override(DPEX_KERNEL_EXP_TARGET_NAME):
+                cres: CompileResult = self._compile_core(args, return_type)
+
+            if (
+                self.targetoptions["_compilation_mode"]
+                == CompilationMode.KERNEL
+            ):
+                kernel_device_ir_module: kl.SPIRVKernelModule = (
+                    self._compile_to_spirv(
+                        cres.library, cres.fndesc, cres.target_context
+                    )
+                )
+            else:
+                kernel_device_ir_module = None
+
+            kcres_attrs = []
+
+            for cres_field in cres._fields:
+                cres_attr = getattr(cres, cres_field)
+                if cres_field == "entry_point":
+                    if cres_attr is not None:
+                        raise AssertionError(
+                            "Compiled kernel and device_func should be "
+                            "compiled with compile_cfunc option turned off"
+                        )
+                    cres_attr = cres.fndesc.qualname
+                kcres_attrs.append(cres_attr)
+
+            kcres_attrs.append(kernel_device_ir_module)
+
+            if config.DUMP_KERNEL_LLVM:
+                with open(
+                    cres.fndesc.llvm_func_name + ".ll",
+                    "w",
+                    encoding="UTF-8",
+                ) as fptr:
+                    fptr.write(cres.library.final_module)
+
+        except errors.TypingError as err:
+            self._failed_cache[key] = err
+            return False, err
+
+        return True, _KernelCompileResult(*kcres_attrs)
+
+
+class KernelDispatcher(Dispatcher):
+    """Dispatcher class designed to compile kernel decorated functions. The
+    dispatcher inherits the Numba Dispatcher class, but has a different
+    compilation strategy. Instead of compiling a kernel decorated function to
+    an executable binary, the dispatcher compiles it to SPIR-V and then caches
+    that SPIR-V bitcode.
+
+    """
+
+    targetdescr = dpex_exp_kernel_target
+    _fold_args = False
+
+    def __init__(
+        self,
+        pyfunc,
+        local_vars_to_numba_types=None,
+        targetoptions=None,
+        pipeline_class=kernel_compiler.KernelCompiler,
+    ):
+        if targetoptions is None:
+            targetoptions = {}
+
+        if local_vars_to_numba_types is None:
+            local_vars_to_numba_types = {}
+
+        targetoptions["nopython"] = True
+        targetoptions["experimental"] = True
+
+        self._kernel_name = pyfunc.__name__
+
+        if numba_sem_version < (0, 59, 0):
+            super().__init__(
+                py_func=pyfunc,
+                locals=local_vars_to_numba_types,
+                impl_kind="direct",
+                targetoptions=targetoptions,
+                pipeline_class=pipeline_class,
+            )
+        else:
+            super().__init__(
+                py_func=pyfunc,
+                locals=local_vars_to_numba_types,
+                targetoptions=targetoptions,
+                pipeline_class=pipeline_class,
+            )
+        self._compiler = _KernelCompiler(
+            pyfunc,
+            self.targetdescr,
+            targetoptions,
+            local_vars_to_numba_types,
+            pipeline_class,
+        )
+
+    def typeof_pyval(self, val):
+        """
+        Resolve the Numba type of Python value *val*.
+        This is called from numba._dispatcher as a fallback if the native code
+        cannot decide the type.
+        """
+        # Not going through the resolve_argument_type() indirection
+        # can save a couple µs.
+        try:
+            typ = typeof(val, Purpose.argument)
+            if isinstance(typ, types.Array) and not isinstance(typ, USMNdArray):
+                raise UnsupportedKernelArgumentError(
+                    type=str(type(val)), value=val
+                )
+        except ValueError:
+            typ = types.pyobject
+        else:
+            if typ is None:
+                typ = types.pyobject
+        self._types_active_call.append(typ)
+        return typ
+
+    def add_overload(self, cres):
+        args = tuple(cres.signature.args)
+        self.overloads[args] = cres
+
+    def compile(self, sig) -> any:
+        disp = self._get_dispatcher_for_current_target()
+        if disp is not self:
+            return disp.compile(sig)
+
+        with ExitStack() as scope:
+            cres = None
+
+            def cb_compiler(dur):
+                if cres is not None:
+                    self._callback_add_compiler_timer(dur, cres)
+
+            def cb_llvm(dur):
+                if cres is not None:
+                    self._callback_add_llvm_timer(dur, cres)
+
+            scope.enter_context(
+                ev.install_timer("numba:compiler_lock", cb_compiler)
+            )
+            scope.enter_context(ev.install_timer("numba:llvm_lock", cb_llvm))
+            scope.enter_context(global_compiler_lock)
+
+            if not self._can_compile:
+                raise RuntimeError("compilation disabled")
+            # Use counter to track recursion compilation depth
+            with self._compiling_counter:
+                args, return_type = sigutils.normalize_signature(sig)
+
+                if (
+                    self.targetoptions["_compilation_mode"]
+                    == CompilationMode.KERNEL
+                ):
+                    # Compute follows data based queue equivalence is only
+                    # evaluated for kernel functions whose arguments are
+                    # supposed to be arrays. For device_func decorated
+                    # functions, the arguments can be scalar and we skip queue
+                    # equivalence check.
+                    try:
+                        self._compiler.check_arguments(self._kernel_name, args)
+                    except ExecutionQueueInferenceError as eqie:
+                        raise eqie
+
+                    # A function being compiled in the KERNEL compilation mode
+                    # cannot have a non-void return value
+                    if return_type and return_type != void:
+                        raise KernelHasReturnValueError(
+                            kernel_name=None, return_type=return_type, sig=sig
+                        )
+
+                # Don't recompile if signature already exists
+                existing = self.overloads.get(tuple(args))
+                if existing is not None:
+                    return existing.entry_point
+
+                # TODO: Enable caching
+                # Add code to enable on disk caching of a binary spirv kernel.
+                # Refer: https://github.com/IntelPython/numba-dpex/issues/1197
+                self._cache_misses[sig] += 1
+                with ev.trigger_event(
+                    "numba_dpex:compile",
+                    data={
+                        "dispatcher": self,
+                        "args": args,
+                        "return_type": return_type,
+                    },
+                ):
+                    try:
+                        compiler: _KernelCompiler = self._compiler
+                        kcres: _KernelCompileResult = compiler.compile(
+                            args, return_type
+                        )
+                    except errors.ForceLiteralArg as err:
+
+                        def folded(args, kws):
+                            return self._compiler.fold_argument_types(
+                                args, kws
+                            )[1]
+
+                        raise err.bind_fold_arguments(folded)
+                    self.add_overload(kcres)
+
+                    kcres.target_context.insert_user_function(
+                        kcres.entry_point, kcres.fndesc, [kcres.library]
+                    )
+
+                # TODO: enable caching of kernel_module
+                # https://github.com/IntelPython/numba-dpex/issues/1197
+
+                return kcres.entry_point
+
+    def __getitem__(self, args):
+        """Square-bracket notation for configuring launch arguments is not
+        supported.
+        """
+
+        raise NotImplementedError
+
+    def __call__(self, *args, **kw_args):
+        """Functor to launch a kernel."""
+
+        raise NotImplementedError
+
+
+_dpex_target = target_registry[DPEX_KERNEL_EXP_TARGET_NAME]
+dispatcher_registry[_dpex_target] = KernelDispatcher
diff --git a/numba_dpex/experimental/kernel_iface/__init__.py b/numba_dpex/experimental/kernel_iface/__init__.py
new file mode 100644
index 0000000000..4433756733
--- /dev/null
+++ b/numba_dpex/experimental/kernel_iface/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""The kernel_iface provides a set of Python classes and functions that are
+analogous to the C++ SYCL API. The kernel_iface API is meant to allow
+prototyping SYCL-like kernels in pure Python before compiling them using
+numba_dpex.kernel.
+"""
+
+from .atomic_ref import AtomicRef
+from .memory_enums import AddressSpace, MemoryOrder, MemoryScope
+
+__all__ = ["AddressSpace", "AtomicRef", "MemoryOrder", "MemoryScope"]
diff --git a/numba_dpex/experimental/kernel_iface/atomic_ref.py b/numba_dpex/experimental/kernel_iface/atomic_ref.py
new file mode 100644
index 0000000000..2090e39757
--- /dev/null
+++ b/numba_dpex/experimental/kernel_iface/atomic_ref.py
@@ -0,0 +1,169 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Implements a mock Python class to represent ``sycl::atomic_ref`` for
+prototyping numba_dpex kernel functions before they are JIT compiled.
+"""
+
+from .memory_enums import AddressSpace, MemoryOrder, MemoryScope
+
+
+class AtomicRef:
+    """Analogue to the ``sycl::atomic_ref`` type. An atomic reference is a
+    view into a data container that can be then updated atomically using any of
+    the ``fetch_*`` member functions of the class.
+    """
+
+    def __init__(  # pylint: disable=too-many-arguments
+        self,
+        ref,
+        index=0,
+        memory_order=MemoryOrder.RELAXED,
+        memory_scope=MemoryScope.DEVICE,
+        address_space=AddressSpace.GLOBAL,
+    ):
+        """A Python stub to represent a SYCL AtomicRef class. An AtomicRef
+        object represents a single element view into a Python array-like object
+        that can be then updated using various fetch_* type functions.
+
+        To maintain analogy with SYCL's API, the AtomicRef constructor takes
+        optional ``memory_order``, ``memory_scope``, and ``address_space``
+        arguments that are ignored in Python execution.
+        """
+        self._memory_order = memory_order
+        self._memory_scope = memory_scope
+        self._address_space = address_space
+        self._ref = ref
+        self._index = index
+
+    @property
+    def ref(self):
+        """Returns the value stored in the AtomicRef._ref[_index]."""
+        return self._ref[self._index]
+
+    @property
+    def memory_order(self) -> MemoryOrder:
+        """Returns the MemoryOrder value used to define the AtomicRef."""
+        return self._memory_order
+
+    @property
+    def memory_scope(self) -> MemoryScope:
+        """Returns the MemoryScope value used to define the AtomicRef."""
+        return self._memory_scope
+
+    @property
+    def address_space(self) -> AddressSpace:
+        """Returns the AddressSpace value used to define the AtomicRef."""
+        return self._address_space
+
+    def fetch_add(self, val):
+        """Adds the operand ``val`` to the object referenced by the AtomicRef
+        and assigns the result to the value of the referenced object. Returns
+        the original value of the object.
+
+        Args:
+            val : Value to be added to the object referenced by the AtomicRef.
+
+        Returns: The original value of the object referenced by the AtomicRef.
+
+        """
+        old = self._ref[self._index]
+        self._ref[self._index] += val
+        return old
+
+    def fetch_sub(self, val):
+        """Subtracts the operand ``val`` to the object referenced by the
+        AtomicRef and assigns the result to the value of the referenced object.
+        Returns the original value of the object.
+
+        Args:
+            val : Value to be subtracted to the object referenced by the
+            AtomicRef.
+
+        Returns: The original value of the object referenced by the AtomicRef.
+
+        """
+        old = self._ref[self._index]
+        self._ref[self._index] -= val
+        return old
+
+    def fetch_min(self, val):
+        """Calculates the minimum value of the operand ``val`` and the object
+        referenced by the AtomicRef and assigns the result to the value of the
+        referenced object. Returns the original value of the object.
+
+        Args:
+            val : Value to be compared against to the object referenced by the
+            AtomicRef.
+
+        Returns: The original value of the object referenced by the AtomicRef.
+
+        """
+        old = self._ref[self._index]
+        self._ref[self._index] = min(old, val)
+        return old
+
+    def fetch_max(self, val):
+        """Calculates the maximum value of the operand ``val`` and the object
+        referenced by the AtomicRef and assigns the result to the value of the
+        referenced object. Returns the original value of the object.
+
+        Args:
+            val : Value to be compared against to the object referenced by the
+            AtomicRef.
+
+        Returns: The original value of the object referenced by the AtomicRef.
+
+        """
+        old = self._ref[self._index]
+        self._ref[self._index] = max(old, val)
+        return old
+
+    def fetch_and(self, val):
+        """Calculates the bitwise AND of the operand ``val`` and the object
+        referenced by the AtomicRef and assigns the result to the value of the
+        referenced object. Returns the original value of the object.
+
+        Args:
+            val : Value to be bitwise ANDed against to the object referenced by
+            the AtomicRef.
+
+        Returns: The original value of the object referenced by the AtomicRef.
+
+        """
+        old = self._ref[self._index]
+        self._ref[self._index] &= val
+        return old
+
+    def fetch_or(self, val):
+        """Calculates the bitwise OR of the operand ``val`` and the object
+        referenced by the AtomicRef and assigns the result to the value of the
+        referenced object. Returns the original value of the object.
+
+        Args:
+            val : Value to be bitwise ORed against to the object referenced by
+            the AtomicRef.
+
+        Returns: The original value of the object referenced by the AtomicRef.
+
+        """
+        old = self._ref[self._index]
+        self._ref[self._index] |= val
+        return old
+
+    def fetch_xor(self, val):
+        """Calculates the bitwise XOR of the operand ``val`` and the object
+        referenced by the AtomicRef and assigns the result to the value of the
+        referenced object. Returns the original value of the object.
+
+        Args:
+            val : Value to be bitwise XORed against to the object referenced by
+            the AtomicRef.
+
+        Returns: The original value of the object referenced by the AtomicRef.
+
+        """
+        old = self._ref[self._index]
+        self._ref[self._index] ^= val
+        return old
diff --git a/numba_dpex/experimental/kernel_iface/memory_enums.py b/numba_dpex/experimental/kernel_iface/memory_enums.py
new file mode 100644
index 0000000000..c65342f008
--- /dev/null
+++ b/numba_dpex/experimental/kernel_iface/memory_enums.py
@@ -0,0 +1,79 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""A collection of FlagEnum classes that syntactically represents the SYCL
+memory enum classes.
+"""
+
+from numba_dpex.experimental.flag_enum import FlagEnum
+
+
+class MemoryOrder(FlagEnum):
+    """
+    An enumeration of the supported ``sycl::memory_order`` values in dpcpp. The
+    integer values of the enums is kept consistent with the corresponding
+    implementation in dpcpp.
+
+    =====================   ============
+    Order                   Enum value
+    =====================   ============
+    RELAXED                 0
+    ACQUIRE                 1
+    CONSUME_UNSUPPORTED     2
+    RELEASE                 3
+    ACQ_REL                 4
+    SEQ_CST                 5
+    =====================   ============
+    """
+
+    RELAXED = 0
+    ACQUIRE = 1
+    CONSUME_UNSUPPORTED = 2
+    RELEASE = 3
+    ACQ_REL = 4
+    SEQ_CST = 5
+
+
+class MemoryScope(FlagEnum):
+    """
+    An enumeration of SYCL memory scope. For more details please refer to
+    SYCL 2020 specification, section 3.8.3.2
+
+    ===============  ============
+    Memory Scope     Enum value
+    ===============  ============
+    WORK_ITEM        0
+    SUB_GROUP        1
+    WORK_GROUP       2
+    DEVICE           3
+    SYSTEM           4
+    ===============  ============
+    """
+
+    WORK_ITEM = 0
+    SUB_GROUP = 1
+    WORK_GROUP = 2
+    DEVICE = 3
+    SYSTEM = 4
+
+
+class AddressSpace(FlagEnum):
+    """The address space values supported by numba_dpex.
+
+    ==================   ============
+    Address space        Value
+    ==================   ============
+    PRIVATE              0
+    GLOBAL               1
+    CONSTANT             2
+    LOCAL                3
+    GENERIC              4
+    ==================   ============
+    """
+
+    PRIVATE = 0
+    GLOBAL = 1
+    CONSTANT = 2
+    LOCAL = 3
+    GENERIC = 4
diff --git a/numba_dpex/experimental/launcher.py b/numba_dpex/experimental/launcher.py
new file mode 100644
index 0000000000..503a8bafe7
--- /dev/null
+++ b/numba_dpex/experimental/launcher.py
@@ -0,0 +1,252 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Provides a helper function to call a numba_dpex.kernel decorated function
+from either CPython or a numba_dpex.dpjit decorated function.
+"""
+
+from typing import NamedTuple, Union
+
+import dpctl
+from llvmlite import ir as llvmir
+from numba.core import cgutils, types
+from numba.core.cpu import CPUContext
+from numba.core.types.containers import Tuple, UniTuple
+from numba.core.types.functions import Dispatcher
+from numba.extending import intrinsic
+
+from numba_dpex import dpjit
+from numba_dpex.core.targets.dpjit_target import DPEX_TARGET_NAME
+from numba_dpex.core.targets.kernel_target import DpexKernelTargetContext
+from numba_dpex.core.types import DpctlSyclEvent, NdRangeType, RangeType
+from numba_dpex.core.utils import kernel_launcher as kl
+from numba_dpex.dpctl_iface import libsyclinterface_bindings as sycl
+from numba_dpex.dpctl_iface.wrappers import wrap_event_reference
+from numba_dpex.experimental.kernel_dispatcher import (
+    KernelDispatcher,
+    _KernelCompileResult,
+)
+
+
+class LLRange(NamedTuple):
+    """Analog of Range and NdRange but for the llvm ir values."""
+
+    global_range_extents: list
+    local_range_extents: list
+
+
+def wrap_event_reference_tuple(ctx, builder, event1, event2):
+    """Creates tuple data model from two event data models, so it can be
+    boxed to Python."""
+    ty_event = DpctlSyclEvent()
+    tupty = types.Tuple([ty_event, ty_event])
+
+    lltupty = ctx.get_value_type(tupty)
+    tup = cgutils.get_null_value(lltupty)
+    tup = builder.insert_value(tup, event1, 0)
+    tup = builder.insert_value(tup, event2, 1)
+
+    return tup
+
+
+@intrinsic(target=DPEX_TARGET_NAME)
+def _submit_kernel_async(
+    typingctx,
+    ty_kernel_fn: Dispatcher,
+    ty_index_space: Union[RangeType, NdRangeType],
+    ty_dependent_events: UniTuple,
+    ty_kernel_args_tuple: UniTuple,
+):
+    """Generates IR code for call_kernel_async ``dpjit`` function."""
+    return _submit_kernel(
+        typingctx,
+        ty_kernel_fn,
+        ty_index_space,
+        ty_dependent_events,
+        ty_kernel_args_tuple,
+        sync=False,
+    )
+
+
+@intrinsic(target=DPEX_TARGET_NAME)
+def _submit_kernel_sync(
+    typingctx,
+    ty_kernel_fn: Dispatcher,
+    ty_index_space: Union[RangeType, NdRangeType],
+    ty_kernel_args_tuple: UniTuple,
+):
+    """Generates IR code for call_kernel ``dpjit`` function."""
+    return _submit_kernel(
+        typingctx,
+        ty_kernel_fn,
+        ty_index_space,
+        None,
+        ty_kernel_args_tuple,
+        sync=True,
+    )
+
+
+def _submit_kernel(  # pylint: disable=too-many-arguments
+    typingctx,  # pylint: disable=unused-argument
+    ty_kernel_fn: Dispatcher,
+    ty_index_space: Union[RangeType, NdRangeType],
+    ty_dependent_events: UniTuple,
+    ty_kernel_args_tuple: UniTuple,
+    sync: bool,
+):
+    """Generates IR code for call_kernel_{async|sync} ``dpjit`` function.
+
+    The intrinsic first compiles the kernel function to SPIR-V, and then to a
+    SYCL kernel bundle. The arguments to the kernel are also packed into
+    flattened arrays and the SYCL queue to which the kernel will be submitted
+    extracted from the args. Finally, the actual kernel is extracted from the
+    kernel bundle and submitted to the SYCL queue.
+
+    If sync set to False, it acquires memory infos from kernel arguments to
+    prevent garbage collection on them. Then it schedules host task to release
+    that arguments and unblock garbage collection. Tuple of host task and device
+    tasks are returned.
+    """
+    # signature of this intrinsic
+    ty_return = types.void
+    if not sync:
+        ty_event = DpctlSyclEvent()
+        ty_return = types.Tuple([ty_event, ty_event])
+
+    if ty_dependent_events is not None:
+        if not isinstance(ty_dependent_events, UniTuple) and not isinstance(
+            ty_dependent_events, Tuple
+        ):
+            raise ValueError("dependent events must be passed as a tuple")
+
+        sig = ty_return(
+            ty_kernel_fn,
+            ty_index_space,
+            ty_dependent_events,
+            ty_kernel_args_tuple,
+        )
+    else:
+        sig = ty_return(ty_kernel_fn, ty_index_space, ty_kernel_args_tuple)
+
+    kernel_sig = types.void(*ty_kernel_args_tuple)
+    # ty_kernel_fn is type specific to exact function, so we can get function
+    # directly from type and compile it. Thats why we don't need to get it in
+    # codegen
+    kernel_dispatcher: KernelDispatcher = ty_kernel_fn.dispatcher
+    kcres: _KernelCompileResult = kernel_dispatcher.get_compile_result(
+        kernel_sig
+    )
+    kernel_module: kl.SPIRVKernelModule = kcres.kernel_device_ir_module
+    kernel_targetctx: DpexKernelTargetContext = kernel_dispatcher.targetctx
+
+    def codegen(
+        cgctx: CPUContext, builder: llvmir.IRBuilder, sig, llargs: list
+    ):
+        ty_index_space: Union[RangeType, NdRangeType] = sig.args[1]
+        ll_index_space: llvmir.Instruction = llargs[1]
+        ty_kernel_args_tuple: UniTuple = sig.args[-1]
+        ll_kernel_args_tuple: llvmir.Instruction = llargs[-1]
+
+        if len(llargs) == 4:
+            ty_dependent_events: UniTuple = sig.args[2]
+            ll_dependent_events: llvmir.Instruction = llargs[2]
+        else:
+            ty_dependent_events = None
+
+        kl_builder = kl.KernelLaunchIRBuilder(
+            cgctx,
+            builder,
+            kernel_targetctx.data_model_manager,
+        )
+        kl_builder.set_range_from_indexer(
+            ty_indexer_arg=ty_index_space,
+            ll_index_arg=ll_index_space,
+        )
+        kl_builder.set_arguments_form_tuple(
+            ty_kernel_args_tuple, ll_kernel_args_tuple
+        )
+        kl_builder.set_queue_from_arguments()
+        kl_builder.set_kernel_from_spirv(kernel_module)
+        if ty_dependent_events is None:
+            kl_builder.set_dependent_events([])
+        else:
+            kl_builder.set_dependent_events_from_tuple(
+                ty_dependent_events,
+                ll_dependent_events,
+            )
+        device_event_ref = kl_builder.submit()
+
+        if not sync:
+            host_event_ref = kl_builder.acquire_meminfo_and_submit_release()
+
+            return wrap_event_reference_tuple(
+                cgctx,
+                builder,
+                wrap_event_reference(cgctx, builder, host_event_ref),
+                wrap_event_reference(cgctx, builder, device_event_ref),
+            )
+
+        sycl.dpctl_event_wait(builder, device_event_ref)
+        sycl.dpctl_event_delete(builder, device_event_ref)
+
+        return None
+
+    return sig, codegen
+
+
+@dpjit
+def call_kernel(kernel_fn, index_space, *kernel_args) -> None:
+    """Calls a numba_dpex.kernel decorated function from CPython or from another
+    dpjit function. Kernel execution happens in synchronous way, so the thread
+    will be blocked till the kernel done execution.
+
+    Args:
+        kernel_fn (numba_dpex.experimental.KernelDispatcher): A
+        numba_dpex.kernel decorated function that is compiled to a
+        KernelDispatcher by numba_dpex.
+        index_space (Range | NdRange): A numba_dpex.Range or numba_dpex.NdRange
+        type object that specifies the index space for the kernel.
+        kernel_args : List of objects that are passed to the numba_dpex.kernel
+        decorated function.
+    """
+    _submit_kernel_sync(  # pylint: disable=E1120
+        kernel_fn,
+        index_space,
+        kernel_args,
+    )
+
+
+@dpjit
+def call_kernel_async(
+    kernel_fn,
+    index_space,
+    dependent_events: list[dpctl.SyclEvent],
+    *kernel_args
+) -> tuple[dpctl.SyclEvent, dpctl.SyclEvent]:
+    """Calls a numba_dpex.kernel decorated function from CPython or from another
+    dpjit function. Kernel execution happens in asynchronous way, so the thread
+    will not be blocked till the kernel done execution. That means that it is
+    user responsibility to properly use any memory used by kernel until the
+    kernel execution is completed.
+
+    Args:
+        kernel_fn (numba_dpex.experimental.KernelDispatcher): A
+        numba_dpex.kernel decorated function that is compiled to a
+        KernelDispatcher by numba_dpex.
+        index_space (Range | NdRange): A numba_dpex.Range or numba_dpex.NdRange
+        type object that specifies the index space for the kernel.
+        kernel_args : List of objects that are passed to the numba_dpex.kernel
+        decorated function.
+
+    Returns:
+        pair of host event and device event. Host event represent host task
+        that releases use of any kernel argument so it can be deallocated.
+        This task may be executed only after device task is done.
+    """
+    return _submit_kernel_async(  # pylint: disable=E1120
+        kernel_fn,
+        index_space,
+        dependent_events,
+        kernel_args,
+    )
diff --git a/numba_dpex/experimental/literal_intenum_type.py b/numba_dpex/experimental/literal_intenum_type.py
new file mode 100644
index 0000000000..64769b45cb
--- /dev/null
+++ b/numba_dpex/experimental/literal_intenum_type.py
@@ -0,0 +1,55 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Definition of a new Literal type in numba-dpex that allows treating IntEnum
+members as integer literals inside a JIT compiled function.
+"""
+from enum import IntEnum
+
+from numba.core.pythonapi import box
+from numba.core.typeconv import Conversion
+from numba.core.types import Integer, Literal
+from numba.core.typing.typeof import typeof
+
+from numba_dpex.core.exceptions import IllegalIntEnumLiteralValueError
+from numba_dpex.experimental.flag_enum import FlagEnum
+
+
+class IntEnumLiteral(Literal, Integer):
+    """A Literal type for IntEnum objects. The type contains the original Python
+    value of the IntEnum class in it.
+    """
+
+    #  pylint: disable=W0231
+    def __init__(self, value):
+        self._literal_init(value)
+        self.name = f"Literal[IntEnum]({value})"
+        if issubclass(value, FlagEnum):
+            basetype = typeof(value.basetype())
+            Integer.__init__(
+                self,
+                name=self.name,
+                bitwidth=basetype.bitwidth,
+                signed=basetype.signed,
+            )
+        else:
+            raise IllegalIntEnumLiteralValueError
+
+    def can_convert_to(self, typingctx, other) -> bool:
+        conv = typingctx.can_convert(self.literal_type, other)
+        if conv is not None:
+            return max(conv, Conversion.promote)
+        return False
+
+
+Literal.ctor_map[IntEnum] = IntEnumLiteral
+
+
+@box(IntEnumLiteral)
+def box_literal_integer(typ, val, ctx):
+    """Defines how a Numba representation for an IntEnumLiteral object should
+    be converted to a PyObject* object and returned back to Python.
+    """
+    val = ctx.context.cast(ctx.builder, val, typ, typ.literal_type)
+    return ctx.box(typ.literal_type, val)
diff --git a/numba_dpex/experimental/models.py b/numba_dpex/experimental/models.py
new file mode 100644
index 0000000000..34c789d507
--- /dev/null
+++ b/numba_dpex/experimental/models.py
@@ -0,0 +1,69 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Provides the Numba data models for the numba_dpex types introduced in the
+numba_dpex.experimental module.
+"""
+
+from llvmlite import ir as llvmir
+from numba.core import types
+from numba.core.datamodel import DataModelManager, models
+from numba.core.datamodel.models import PrimitiveModel, StructModel
+from numba.core.extending import register_model
+
+import numba_dpex.core.datamodel.models as dpex_core_models
+
+from .dpcpp_types import AtomicRefType
+from .literal_intenum_type import IntEnumLiteral
+from .types import KernelDispatcherType
+
+
+class AtomicRefModel(StructModel):
+    """Data model for AtomicRefType."""
+
+    def __init__(self, dmm, fe_type):
+        members = [
+            (
+                "ref",
+                types.CPointer(fe_type.dtype, addrspace=fe_type.address_space),
+            ),
+        ]
+        super().__init__(dmm, fe_type, members)
+
+
+class IntEnumLiteralModel(PrimitiveModel):
+    """Representation of an object of LiteralIntEnum type using Numba's
+    PrimitiveModel that can be represented natively in the target in all
+    usage contexts.
+    """
+
+    def __init__(self, dmm, fe_type):
+        be_type = llvmir.IntType(fe_type.bitwidth)
+        super().__init__(dmm, fe_type, be_type)
+
+
+def _init_exp_data_model_manager() -> DataModelManager:
+    """Initializes a DpexExpKernelTarget-specific data model manager.
+
+    Extends the DpexKernelTargetContext's datamodel manager with all
+    experimental types that are getting added to the kernel API.
+
+    Returns:
+        DataModelManager: A numba-dpex DpexExpKernelTarget-specific data model
+        manager
+    """
+
+    dmm = dpex_core_models.dpex_data_model_manager.copy()
+
+    # Register the types and data model in the DpexExpTargetContext
+    dmm.register(IntEnumLiteral, IntEnumLiteralModel)
+    dmm.register(AtomicRefType, AtomicRefModel)
+
+    return dmm
+
+
+exp_dmm = _init_exp_data_model_manager()
+
+# Register any new type that should go into numba.core.datamodel.default_manager
+register_model(KernelDispatcherType)(models.OpaqueModel)
diff --git a/numba_dpex/experimental/target.py b/numba_dpex/experimental/target.py
new file mode 100644
index 0000000000..7eb3594264
--- /dev/null
+++ b/numba_dpex/experimental/target.py
@@ -0,0 +1,150 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""A new target descriptor that includes experimental features that should
+eventually move into the numba_dpex.core.
+"""
+
+from functools import cached_property
+
+from llvmlite import ir as llvmir
+from numba.core import types
+from numba.core.descriptors import TargetDescriptor
+from numba.core.target_extension import GPU, target_registry
+from numba.core.types.scalars import IntEnumClass
+
+from numba_dpex.core.descriptor import DpexTargetOptions
+from numba_dpex.core.targets.kernel_target import (
+    DpexKernelTargetContext,
+    DpexKernelTypingContext,
+)
+from numba_dpex.experimental.models import exp_dmm
+
+from .flag_enum import FlagEnum
+from .literal_intenum_type import IntEnumLiteral
+
+
+#  pylint: disable=R0903
+class SyclDeviceExp(GPU):
+    """Mark the hardware target as SYCL Device."""
+
+
+DPEX_KERNEL_EXP_TARGET_NAME = "dpex_kernel_exp"
+
+target_registry[DPEX_KERNEL_EXP_TARGET_NAME] = SyclDeviceExp
+
+
+class DpexExpKernelTypingContext(DpexKernelTypingContext):
+    """Experimental typing context class extending the DpexKernelTypingContext
+    by overriding super class functions for new experimental types.
+
+    A new experimental type may require updating type inference for that type
+    when it is used as an argument, value or attribute in a JIT compiled
+    function. All such experimental functionality should be added here till they
+    are stable enough to be migrated to DpexKernelTypingContext.
+    """
+
+    def resolve_value_type(self, val):
+        """
+        Return the numba type of a Python value that is being used
+        as a runtime constant.
+        ValueError is raised for unsupported types.
+        """
+
+        typ = super().resolve_value_type(val)
+
+        if isinstance(typ, IntEnumClass) and issubclass(val, FlagEnum):
+            typ = IntEnumLiteral(val)
+
+        return typ
+
+    def resolve_getattr(self, typ, attr):
+        """
+        Resolve getting the attribute *attr* (a string) on the Numba type.
+        The attribute's type is returned, or None if resolution failed.
+        """
+        retty = None
+
+        if isinstance(typ, IntEnumLiteral):
+            try:
+                attrval = getattr(typ.literal_value, attr).value
+                retty = types.IntegerLiteral(attrval)
+            except ValueError:
+                pass
+        else:
+            retty = super().resolve_getattr(typ, attr)
+        return retty
+
+
+#  pylint: disable=W0223
+# FIXME: Remove the pylint disablement once we add an override for
+# get_executable
+class DpexExpKernelTargetContext(DpexKernelTargetContext):
+    """Experimental target context class extending the DpexKernelTargetContext
+    by overriding super class functions for new experimental types.
+
+    A new experimental type may require specific ways for handling the lowering
+    to LLVM IR. All such experimental functionality should be added here till
+    they are stable enough to be migrated to DpexKernelTargetContext.
+    """
+
+    allow_dynamic_globals = True
+
+    def __init__(self, typingctx, target=DPEX_KERNEL_EXP_TARGET_NAME):
+        super().__init__(typingctx, target)
+        self.data_model_manager = exp_dmm
+
+    def get_getattr(self, typ, attr):
+        """
+        Overrides the get_getattr function to provide an implementation for
+        getattr call on an IntegerEnumLiteral type.
+        """
+
+        if isinstance(typ, IntEnumLiteral):
+            #  pylint: disable=W0613
+            def enum_literal_getattr_imp(context, builder, typ, val, attr):
+                enum_attr_value = getattr(typ.literal_value, attr).value
+                return llvmir.Constant(llvmir.IntType(64), enum_attr_value)
+
+            return enum_literal_getattr_imp
+
+        return super().get_getattr(typ, attr)
+
+
+class DpexExpKernelTarget(TargetDescriptor):
+    """
+    Implements a target descriptor for numba_dpex.kernel decorated functions.
+    """
+
+    options = DpexTargetOptions
+
+    @cached_property
+    def _toplevel_target_context(self):
+        """Lazily-initialized top-level target context, for all threads."""
+        return DpexExpKernelTargetContext(
+            self.typing_context, self._target_name
+        )
+
+    @cached_property
+    def _toplevel_typing_context(self):
+        """Lazily-initialized top-level typing context, for all threads."""
+        return DpexExpKernelTypingContext()
+
+    @property
+    def target_context(self):
+        """
+        The target context used by the Dpex compiler pipeline.
+        """
+        return self._toplevel_target_context
+
+    @property
+    def typing_context(self):
+        """
+        The typing context for used by the Dpex compiler pipeline.
+        """
+        return self._toplevel_typing_context
+
+
+# A global instance of the DpexKernelTarget with the experimental features
+dpex_exp_kernel_target = DpexExpKernelTarget(DPEX_KERNEL_EXP_TARGET_NAME)
diff --git a/numba_dpex/experimental/typeof.py b/numba_dpex/experimental/typeof.py
new file mode 100644
index 0000000000..e90bfccf0a
--- /dev/null
+++ b/numba_dpex/experimental/typeof.py
@@ -0,0 +1,35 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Adds typeof implementations to Numba registry for all numba-dpex experimental
+types.
+
+"""
+
+from numba.extending import typeof_impl
+
+from .dpcpp_types import AtomicRefType
+from .kernel_iface import AtomicRef
+
+
+@typeof_impl.register(AtomicRef)
+def typeof_atomic_ref(val: AtomicRef, ctx) -> AtomicRefType:
+    """Returns a ``numba_dpex.experimental.dpctpp_types.AtomicRefType``
+    instance for a Python AtomicRef object.
+
+    Args:
+        val (AtomicRef): Instance of the AtomicRef type.
+        ctx : Numba typing context used for type inference.
+
+    Returns: AtomicRefType object corresponding to the AtomicRef object.
+
+    """
+    dtype = typeof_impl(val.ref, ctx)
+
+    return AtomicRefType(
+        dtype=dtype,
+        memory_order=val.memory_order.value,
+        memory_scope=val.memory_scope.value,
+        address_space=val.address_space.value,
+    )
diff --git a/numba_dpex/experimental/types.py b/numba_dpex/experimental/types.py
new file mode 100644
index 0000000000..2400016069
--- /dev/null
+++ b/numba_dpex/experimental/types.py
@@ -0,0 +1,15 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Experimental types that will eventually move to numba_dpex.core.types
+"""
+
+from numba.core import types
+
+
+class KernelDispatcherType(types.Dispatcher):
+    """The type of KernelDispatcher dispatchers"""
+
+    def cast_python_value(self, args):
+        raise NotImplementedError
diff --git a/numba_dpex/numba_patches/__init__.py b/numba_dpex/numba_patches/__init__.py
index 3a217e6325..1d329c9611 100644
--- a/numba_dpex/numba_patches/__init__.py
+++ b/numba_dpex/numba_patches/__init__.py
@@ -1,3 +1,3 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/numba_patches/patch_arrayexpr_tree_to_ir.py b/numba_dpex/numba_patches/patch_arrayexpr_tree_to_ir.py
index e0d42d71f8..9b675ba38c 100644
--- a/numba_dpex/numba_patches/patch_arrayexpr_tree_to_ir.py
+++ b/numba_dpex/numba_patches/patch_arrayexpr_tree_to_ir.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -46,14 +46,12 @@ def _ufunc_to_parfor_instr(
             g_math_assign = ir.Assign(g_math, g_math_var, loc)
             func_var_def = ir.Expr.getattr(g_math_var, "sqrt", loc)
             out_ir.append(g_math_assign)
-        #                     out_ir.append(func_var_def)
         ir_expr = ir.Expr.call(func_var, arg_vars, (), loc)
         call_typ = typemap[func_var.name].get_call_type(
             typingctx, tuple(typemap[a.name] for a in arg_vars), {}
         )
         calltypes[ir_expr] = call_typ
         el_typ = call_typ.return_type
-        # signature(el_typ, el_typ)
         out_ir.append(ir.Assign(func_var_def, func_var, loc))
         out_ir.append(ir.Assign(ir_expr, expr_out_var, loc))
 
@@ -122,7 +120,6 @@ def _arrayexpr_tree_to_ir(
                 out_ir.append(ir.Assign(ir_expr, expr_out_var, loc))
             for T in array_analysis.MAP_TYPES:
                 if isinstance(op, T):
-                    # elif isinstance(op, (np.ufunc, DUFunc)):
                     # function calls are stored in variables which are not removed
                     # op is typing_key to the variables type
                     el_typ = _ufunc_to_parfor_instr(
@@ -169,7 +166,6 @@ def _arrayexpr_tree_to_ir(
                     out_ir,
                 )
             else:
-                # assert typemap[expr.name]==el_typ
                 el_typ = var_typ
                 ir_expr = expr
             out_ir.append(ir.Assign(ir_expr, expr_out_var, loc))
diff --git a/numba_dpex/numba_patches/patch_is_ufunc.py b/numba_dpex/numba_patches/patch_is_ufunc.py
index ee8b394b5a..dee4a3f375 100644
--- a/numba_dpex/numba_patches/patch_is_ufunc.py
+++ b/numba_dpex/numba_patches/patch_is_ufunc.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2022 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/numba_patches/patch_mk_alloc.py b/numba_dpex/numba_patches/patch_mk_alloc.py
index 792ff7f686..0f4f8f195e 100644
--- a/numba_dpex/numba_patches/patch_mk_alloc.py
+++ b/numba_dpex/numba_patches/patch_mk_alloc.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -77,7 +77,7 @@ def _mk_alloc(
                 out,
             )
 
-        # g_np_var = Global(numpy)
+        # g_np_var = Global(numpy) # noqa: E800
         g_np_var = ir.Var(scope, mk_unique_var("$np_g_var"), loc)
         if typemap:
             typemap[g_np_var.name] = types.misc.Module(numpy)
diff --git a/numba_dpex/ocl/__init__.py b/numba_dpex/ocl/__init__.py
index 00fec5f515..7206ff64a6 100644
--- a/numba_dpex/ocl/__init__.py
+++ b/numba_dpex/ocl/__init__.py
@@ -1,3 +1,3 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/ocl/_declare_function.py b/numba_dpex/ocl/_declare_function.py
index 7fc2396e17..2bb63b9557 100644
--- a/numba_dpex/ocl/_declare_function.py
+++ b/numba_dpex/ocl/_declare_function.py
@@ -1,8 +1,7 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-
 from llvmlite import ir as llvmir
 from numba.core import cgutils, types
 
diff --git a/numba_dpex/ocl/atomics/__init__.py b/numba_dpex/ocl/atomics/__init__.py
index 00fec5f515..7206ff64a6 100644
--- a/numba_dpex/ocl/atomics/__init__.py
+++ b/numba_dpex/ocl/atomics/__init__.py
@@ -1,3 +1,3 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/ocl/atomics/atomic_helper.py b/numba_dpex/ocl/atomics/atomic_helper.py
index 2ebecc6b79..dfa07213ca 100644
--- a/numba_dpex/ocl/atomics/atomic_helper.py
+++ b/numba_dpex/ocl/atomics/atomic_helper.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/ocl/mathdecl.py b/numba_dpex/ocl/mathdecl.py
index 9e5d015497..a9b7e12aca 100644
--- a/numba_dpex/ocl/mathdecl.py
+++ b/numba_dpex/ocl/mathdecl.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -108,9 +108,6 @@ def resolve_degrees(self, mod):
     def resolve_radians(self, mod):
         return types.Function(Math_radians)
 
-    # def resolve_hypot(self, mod):
-    # return types.Function(Math_hypot)
-
     def resolve_copysign(self, mod):
         return types.Function(Math_copysign)
 
@@ -248,16 +245,6 @@ class Math_degrees(Math_unary):
     key = math.degrees
 
 
-# class Math_hypot(ConcreteTemplate):
-# key = math.hypot
-#     cases = [
-#         signature(types.float64, types.int64, types.int64),
-#         signature(types.float64, types.uint64, types.uint64),
-#         signature(types.float32, types.float32, types.float32),
-#         signature(types.float64, types.float64, types.float64),
-#     ]
-
-
 class Math_erf(Math_unary):
     key = math.erf
 
@@ -340,7 +327,6 @@ class Math_isinf(ConcreteTemplate):
 infer_global(math.asinh, types.Function(Math_asinh))
 infer_global(math.acosh, types.Function(Math_acosh))
 infer_global(math.atanh, types.Function(Math_atanh))
-# infer_global(math.hypot, types.Function(Math_hypot))
 infer_global(math.floor, types.Function(Math_floor))
 infer_global(math.ceil, types.Function(Math_ceil))
 infer_global(math.trunc, types.Function(Math_trunc))
diff --git a/numba_dpex/ocl/mathimpl.py b/numba_dpex/ocl/mathimpl.py
index 5c1a37ca77..4cf5719e99 100644
--- a/numba_dpex/ocl/mathimpl.py
+++ b/numba_dpex/ocl/mathimpl.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -160,7 +160,6 @@ def function_name_to_supported_decl(name, sig):
             return None
 
     fn = _mk_fn_decl(name, sig)
-    # lower(key, *sig.args)(fn)
     lower_ocl_impl[(name, sig)] = lower(key, *sig.args)(fn)
 
 
diff --git a/numba_dpex/ocl/ocldecl.py b/numba_dpex/ocl/ocldecl.py
index ea2686a962..ab4c943d03 100644
--- a/numba_dpex/ocl/ocldecl.py
+++ b/numba_dpex/ocl/ocldecl.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -22,8 +22,6 @@
 intrinsic_attr = registry.register_attr
 intrinsic_global = registry.register_global
 
-# register_number_classes(intrinsic_global)
-
 
 @intrinsic
 class Ocl_get_global_id(ConcreteTemplate):
diff --git a/numba_dpex/ocl/oclimpl.py b/numba_dpex/ocl/oclimpl.py
index e47bb215c4..727ed632da 100644
--- a/numba_dpex/ocl/oclimpl.py
+++ b/numba_dpex/ocl/oclimpl.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -12,7 +12,8 @@
 from numba.core.imputils import Registry
 from numba.core.typing.npydecl import parse_dtype
 
-from numba_dpex import config, kernel_target
+from numba_dpex import kernel_target
+from numba_dpex.core import config
 from numba_dpex.core.codegen import SPIR_DATA_LAYOUT
 from numba_dpex.core.types import Array
 from numba_dpex.ocl.atomics import atomic_helper
@@ -420,7 +421,6 @@ def _make_array(
         shape=cgutils.pack_array(builder, kshape),
         strides=cgutils.pack_array(builder, kstrides),
         itemsize=context.get_constant(types.intp, itemsize),
-        meminfo=None,
     )
 
     return ary._getvalue()
diff --git a/numba_dpex/ocl/stubs.py b/numba_dpex/ocl/stubs.py
index 3c0d2162c4..73cca07380 100644
--- a/numba_dpex/ocl/stubs.py
+++ b/numba_dpex/ocl/stubs.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/parfor_diagnostics.py b/numba_dpex/parfor_diagnostics.py
index a54835ef82..ae36cd2582 100644
--- a/numba_dpex/parfor_diagnostics.py
+++ b/numba_dpex/parfor_diagnostics.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -41,7 +41,6 @@ def print_auto_offloading(self, lines):
             tmp.append([])
 
         summary = dict()
-        # region : {fused, serialized}
 
         def print_nest(fadj_, nadj_, theroot, reported, region_id):
             def print_g(fadj_, nadj_, nroot, depth):
diff --git a/numba_dpex/printimpl.py b/numba_dpex/printimpl.py
index 876f3a3706..7df4720ff1 100644
--- a/numba_dpex/printimpl.py
+++ b/numba_dpex/printimpl.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/spirv_generator.py b/numba_dpex/spirv_generator.py
index 75627edb77..7f5c9931c6 100644
--- a/numba_dpex/spirv_generator.py
+++ b/numba_dpex/spirv_generator.py
@@ -1,63 +1,38 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-"""A wrapper to connect to the SPIR-V binaries (Tools, Translator)."""
+"""
+A wrapper to call dpcpp's llvm-spirv tool to generate a SPIR-V binary from
+a numba-dpex generated LLVM IR module.
+"""
 
 import os
 import tempfile
-from subprocess import CalledProcessError, check_call
+from subprocess import STDOUT, CalledProcessError, check_output
 
-from numba_dpex import config
-from numba_dpex.core.targets.kernel_target import LINK_ATOMIC, LLVM_SPIRV_ARGS
+from numba_dpex.core import config
+from numba_dpex.core.exceptions import InternalError
+from numba_dpex.core.targets.kernel_target import LLVM_SPIRV_ARGS
 
 
-def _raise_bad_env_path(msg, path, extra=None):
-    error_message = msg.format(path)
-    if extra is not None:
-        error_message += extra
-    raise ValueError(error_message)
-
-
-_real_check_call = check_call
-
-
-def check_call(*args, **kwargs):
-    return _real_check_call(*args, **kwargs)
-
-
-class CmdLine:
-    def disassemble(self, ipath, opath):
-        """
-        Disassemble a spirv module.
-
-        Args:
-            ipath: Input file path of the spirv module.
-            opath: Output file path of the disassembled spirv module.
-        """
-        flags = []
-        check_call(["spirv-dis", *flags, "-o", opath, ipath])
-
-    def validate(self, ipath):
-        """
-        Validate a spirv module.
-
-        Args:
-            ipath: Input file path of the spirv module.
-        """
-        flags = []
-        check_call(["spirv-val", *flags, ipath])
+def run_cmd(args, error_message=None):
+    try:
+        check_output(
+            args,
+            stderr=STDOUT,
+        )
+    except CalledProcessError as err:
+        if error_message is None:
+            error_message = f"Error during call to {args[0]}"
+        raise InternalError(
+            f"{error_message}:\n\t"
+            + "\t".join(err.output.decode("utf-8").splitlines(True))
+        )
 
-    def optimize(self, ipath, opath):
-        """
-        Optimize a spirv module.
 
-        Args:
-            ipath: Input file path of the spirv module.
-            opath: Output file path of the optimized spirv module.
-        """
-        flags = []
-        check_call(["spirv-opt", *flags, "-o", opath, ipath])
+class _SpirvGenerator:
+    """Generates a SPIR-V binary from supplied LLVM IR."""
 
     def generate(self, llvm_spirv_args, ipath, opath):
         """
@@ -79,7 +54,10 @@ def generate(self, llvm_spirv_args, ipath, opath):
         if config.DEBUG:
             print(f"Use llvm-spirv: {llvm_spirv_tool}")
 
-        check_call([llvm_spirv_tool, *llvm_spirv_args, "-o", opath, ipath])
+        run_cmd(
+            [llvm_spirv_tool, *llvm_spirv_args, "-o", opath, ipath],
+            error_message="Error during lowering LLVM IR to SPIRV",
+        )
 
     @staticmethod
     def _llvm_spirv():
@@ -101,7 +79,7 @@ def __init__(self, context, llvmir, llvmbc):
         """
         self._tmpdir = tempfile.mkdtemp()
         self._tempfiles = []
-        self._cmd = CmdLine()
+        self._generator = _SpirvGenerator()
         self._finalized = False
         self.context = context
 
@@ -142,7 +120,13 @@ def finalize(self):
         # Generate SPIR-V from "friendly" LLVM-based SPIR 2.0
         spirv_path = self._track_temp_file("generated-spirv")
 
-        llvm_spirv_args = []
+        # TODO: find better approach to set SPIRV compiler arguments. Workaround
+        #  against caching intrinsic that sets this argument.
+        # https://github.com/IntelPython/numba-dpex/issues/1262
+        llvm_spirv_args = [
+            "--spirv-ext=+SPV_EXT_shader_atomic_float_add",
+            "--spirv-ext=+SPV_EXT_shader_atomic_float_min_max",
+        ]
         for key in list(self.context.extra_compile_options.keys()):
             if key == LLVM_SPIRV_ARGS:
                 llvm_spirv_args = self.context.extra_compile_options[key]
@@ -162,7 +146,7 @@ def finalize(self):
             print("generated_llvm.bc")
             print("".center(80, "="))
 
-        self._cmd.generate(
+        self._generator.generate(
             llvm_spirv_args=llvm_spirv_args,
             ipath=self._llvmfile,
             opath=spirv_path,
@@ -179,28 +163,7 @@ def finalize(self):
             print("generated_spirv.spir")
             print("".center(80, "="))
 
-        # Validate the SPIR-V code
-        if config.SPIRV_VAL == 1:
-            try:
-                self._cmd.validate(ipath=spirv_path)
-            except CalledProcessError:
-                print("SPIR-V Validation failed...")
-                pass
-            else:
-                # Optimize SPIR-V code
-                opt_path = self._track_temp_file("optimized-spirv")
-                self._cmd.optimize(ipath=spirv_path, opath=opt_path)
-
-                if config.DUMP_ASSEMBLY:
-                    # Disassemble optimized SPIR-V code
-                    dis_path = self._track_temp_file("disassembled-spirv")
-                    self._cmd.disassemble(ipath=opt_path, opath=dis_path)
-                    with open(dis_path, "rb") as fin_opt:
-                        print("ASSEMBLY".center(80, "-"))
-                        print(fin_opt.read())
-                        print("".center(80, "="))
-
-        # Read and return final SPIR-V (not optimized!)
+        # Read and return final SPIR-V
         with open(spirv_path, "rb") as fin:
             spirv = fin.read()
 
diff --git a/numba_dpex/utils/__init__.py b/numba_dpex/utils/__init__.py
index e59643f847..60a29d8f6d 100644
--- a/numba_dpex/utils/__init__.py
+++ b/numba_dpex/utils/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/utils/array_utils.py b/numba_dpex/utils/array_utils.py
index f5d67e9e4b..a4de31c445 100644
--- a/numba_dpex/utils/array_utils.py
+++ b/numba_dpex/utils/array_utils.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -8,7 +8,7 @@
 import dpctl.memory as dpctl_mem
 import numpy as np
 
-from numba_dpex import config
+from numba_dpex.core import config
 
 supported_numpy_dtype = [
     np.int32,
diff --git a/numba_dpex/utils/constants.py b/numba_dpex/utils/constants.py
index 781c0b857c..c2bcf70a59 100644
--- a/numba_dpex/utils/constants.py
+++ b/numba_dpex/utils/constants.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/utils/llvm_codegen_helpers.py b/numba_dpex/utils/llvm_codegen_helpers.py
index 8c1e422dba..4845820c3e 100644
--- a/numba_dpex/utils/llvm_codegen_helpers.py
+++ b/numba_dpex/utils/llvm_codegen_helpers.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/vectorizers.py b/numba_dpex/vectorizers.py
index f0c826f194..819adeef3d 100644
--- a/numba_dpex/vectorizers.py
+++ b/numba_dpex/vectorizers.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -148,7 +148,6 @@ def attempt_ravel(a):
                 return devout.reshape(outshape)
             else:
                 # Otherwise, transfer output back to host
-                # return devout.copy_to_host().reshape(outshape)
                 raise ValueError("copy_to_host() is not yet supported")
 
         elif cr.is_device_array(out):
@@ -195,7 +194,7 @@ def to_host(self, devary, queue):
         copy_to_numpy_from_usm_obj(devary_memview, hostary)
 
     def launch(self, func, count, queue, args):
-        func[count, dpex.DEFAULT_LOCAL_SIZE](*args)
+        func[dpex.Range(count)](*args)
 
     def device_array(self, shape, dtype, queue):
         size = np.prod(shape)
diff --git a/pyproject.toml b/pyproject.toml
index fc81e2609f..10d25af87c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,8 +43,14 @@ exclude_lines = [
 VCS = "git"
 style = "pep440"
 versionfile_source = "numba_dpex/_version.py"
+versionfile_build = "numba_dpex/_version.py"
 parentdir_prefix = ""
 
+[tool.pylint]
+disable = [
+    "fixme"
+]
+
 [build-system]
 requires = [
     "setuptools>=42",
@@ -55,3 +61,19 @@ requires = [
     "versioneer-518"
 ]
 build-backend = "setuptools.build_meta"
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+norecursedirs= [
+    ".*", "*.egg*", "build", "dist", "conda-recipe",
+]
+xfail_strict = true
+addopts = [
+    "--disable-warnings",
+    "--strict",
+    "--durations=20",
+    "-ra -vv",
+]
+testpaths = [
+    "numba_dpex/tests"
+]
diff --git a/scripts/update_copyrights.py b/scripts/update_copyrights.py
index ff3ac851a0..06bec30832 100644
--- a/scripts/update_copyrights.py
+++ b/scripts/update_copyrights.py
@@ -30,4 +30,7 @@ def update_copyrights(root_dir, year):
 path = os.path.dirname(os.path.realpath(__file__))
 source_path = os.path.dirname(path)
 
-update_copyrights(source_path + "/numba_dpex", 2023)
+if __name__ == "__main__":
+    print("Provide new copyright year:")
+    year = input()
+    update_copyrights(source_path + "/numba_dpex", year)
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 810b550257..0000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,21 +0,0 @@
-
-# See the docstring in versioneer.py for instructions. Note that you must
-# re-run 'versioneer.py setup' after changing this section, and commit the
-# resulting files.
-
-[versioneer]
-VCS = git
-style = pep440
-versionfile_source = numba_dpex/_version.py
-versionfile_build = numba_dpex/_version.py
-tag_prefix =
-#parentdir_prefix =
-
-[tool:pytest]
-addopts =
-  --cov
-  --cov-report term
-  ; --cov-report xml
-  ; --cov-report html
-  ; --cov-context test
-  ; --cov-append
diff --git a/setup.py b/setup.py
index adbcf049d2..36f900770c 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,6 @@
 
 
 import re
-import sys
 
 from setuptools import find_packages
 from skbuild import setup
@@ -49,15 +48,6 @@ def to_cmake_format(version: str):
     return match.group(0)
 
 
-# Set is_install and is_develop flags
-is_install = sys.argv[1] == "install" or "bdist_wheel" in sys.argv
-is_develop = sys.argv[1] == "develop"
-
-
-# Test if system is WIN
-is_windows = sys.platform.startswith("win") or sys.platform.startswith("cyg")
-
-
 # Get the project version
 __version__ = versioneer.get_version()
 
@@ -80,7 +70,7 @@ def to_cmake_format(version: str):
         "Environment :: GPU",
         "Environment :: Plugins",
         "Intended Audience :: Developers",
-        "License :: OSI Approved :: Apache 2.0",
+        "License :: OSI Approved :: Apache Software License",
         "Operating System :: OS Independent",
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: Implementation :: CPython",
@@ -98,7 +88,5 @@ def to_cmake_format(version: str):
         "-DNUMBA_DPEX_VERSION:STRING={0:s}".format(
             to_cmake_format(str(__version__))
         ),
-        "-DIS_INSTALL:BOOL={0:s}".format("TRUE" if is_install else "FALSE"),
-        "-DIS_DEVELOP:BOOL={0:s}".format("TRUE" if is_develop else "FALSE"),
     ],
 )


Test patch:
diff --git a/numba_dpex/experimental/testing.py b/numba_dpex/experimental/testing.py
new file mode 100644
index 0000000000..69ce6cdfe0
--- /dev/null
+++ b/numba_dpex/experimental/testing.py
@@ -0,0 +1,32 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Tools for testing, not intended for regular use."""
+
+
+from numba.core import types
+from numba.extending import intrinsic
+
+from numba_dpex import dpjit
+from numba_dpex.core.runtime.context import DpexRTContext
+from numba_dpex.core.targets.dpjit_target import DPEX_TARGET_NAME
+
+
+@intrinsic(target=DPEX_TARGET_NAME)
+def _kernel_cache_size(
+    typingctx,  # pylint: disable=W0613
+):
+    sig = types.int64()
+
+    def codegen(ctx, builder, sig, llargs):  # pylint: disable=W0613
+        dpexrt = DpexRTContext(ctx)
+        return dpexrt.kernel_cache_size(builder)
+
+    return sig, codegen
+
+
+@dpjit
+def kernel_cache_size() -> int:
+    """Returns kernel cache size."""
+    return _kernel_cache_size()  # pylint: disable=E1120
diff --git a/numba_dpex/tests/__init__.py b/numba_dpex/tests/__init__.py
index ba0c9773b5..9604a0af51 100644
--- a/numba_dpex/tests/__init__.py
+++ b/numba_dpex/tests/__init__.py
@@ -1,6 +1,6 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 import numba_dpex
-from numba_dpex import config
+from numba_dpex.core import config
diff --git a/numba_dpex/tests/_helper.py b/numba_dpex/tests/_helper.py
index cb9a166f9b..e5382eac69 100644
--- a/numba_dpex/tests/_helper.py
+++ b/numba_dpex/tests/_helper.py
@@ -1,17 +1,29 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 import contextlib
 import shutil
+from functools import cache
 
 import dpctl
 import dpnp
 import pytest
 
-from numba_dpex import config, numba_sem_version
+from numba_dpex import dpjit, numba_sem_version
+from numba_dpex.core import config
+
+
+@cache
+def has_numba_mlir():
+    try:
+        import numba_mlir
+    except ImportError:
+        return False
+
+    return True
 
 
 def has_opencl_gpu():
@@ -89,6 +101,10 @@ def is_windows():
     not has_level_zero(),
     reason="No level-zero GPU platforms available",
 )
+skip_no_numba_mlir = pytest.mark.skipif(
+    not has_numba_mlir(),
+    reason="numba-mlir package is not availabe",
+)
 
 filter_strings = [
     pytest.param("level_zero:gpu:0", marks=skip_no_level_zero_gpu),
@@ -123,6 +139,14 @@ def is_windows():
 )
 
 
+decorators = [
+    pytest.param(dpjit, id="dpjit"),
+    pytest.param(
+        dpjit(use_mlir=True), id="dpjit_mlir", marks=skip_no_numba_mlir
+    ),
+]
+
+
 @contextlib.contextmanager
 def override_config(name, value, config=config):
     """
diff --git a/numba_dpex/tests/conftest.py b/numba_dpex/tests/conftest.py
index 8fbbcaa238..c5ed0030ed 100644
--- a/numba_dpex/tests/conftest.py
+++ b/numba_dpex/tests/conftest.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/__init__.py b/numba_dpex/tests/core/__init__.py
index 5ca1120d9e..02650dffb8 100644
--- a/numba_dpex/tests/core/__init__.py
+++ b/numba_dpex/tests/core/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/passes/__init__.py b/numba_dpex/tests/core/passes/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/core/passes/__init__.py
+++ b/numba_dpex/tests/core/passes/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/passes/test_parfor_legalize_cfd_pass.py b/numba_dpex/tests/core/passes/test_parfor_legalize_cfd_pass.py
index fa1078b4d9..34a4a39797 100644
--- a/numba_dpex/tests/core/passes/test_parfor_legalize_cfd_pass.py
+++ b/numba_dpex/tests/core/passes/test_parfor_legalize_cfd_pass.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/runtime/__init__.py b/numba_dpex/tests/core/runtime/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/core/runtime/__init__.py
+++ b/numba_dpex/tests/core/runtime/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/runtime/test_llvm_registration.py b/numba_dpex/tests/core/runtime/test_llvm_registration.py
index 20c9f9a5c3..3194ef7ebf 100644
--- a/numba_dpex/tests/core/runtime/test_llvm_registration.py
+++ b/numba_dpex/tests/core/runtime/test_llvm_registration.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/test_dpjit_target.py b/numba_dpex/tests/core/test_dpjit_target.py
index a9c74d409a..b77645c1f4 100644
--- a/numba_dpex/tests/core/test_dpjit_target.py
+++ b/numba_dpex/tests/core/test_dpjit_target.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/test_itanium_mangler_extension.py b/numba_dpex/tests/core/test_itanium_mangler_extension.py
index eda6c2156c..3244dc58a3 100644
--- a/numba_dpex/tests/core/test_itanium_mangler_extension.py
+++ b/numba_dpex/tests/core/test_itanium_mangler_extension.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/types/DpctlSyclEvent/__init__.py b/numba_dpex/tests/core/types/DpctlSyclEvent/__init__.py
new file mode 100644
index 0000000000..fa6c31ef7c
--- /dev/null
+++ b/numba_dpex/tests/core/types/DpctlSyclEvent/__init__.py
@@ -0,0 +1,5 @@
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from . import *
diff --git a/numba_dpex/tests/core/types/DpctlSyclEvent/test_box.py b/numba_dpex/tests/core/types/DpctlSyclEvent/test_box.py
new file mode 100644
index 0000000000..3b0d5e2c70
--- /dev/null
+++ b/numba_dpex/tests/core/types/DpctlSyclEvent/test_box.py
@@ -0,0 +1,42 @@
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Tests for boxing and allocating for dpctl.SyclEvent
+"""
+
+import sys
+
+from dpctl import SyclEvent
+
+from numba_dpex import dpjit
+
+
+def test_dpjit_constructor():
+    """Test event delete that does not have parent"""
+
+    @dpjit
+    def func() -> SyclEvent:
+        SyclEvent()
+        return None
+
+    # We just want to make sure execution did not crush. There are currently
+    # no way to check if event wast destroyed, except manual run with debug
+    # logs on.
+    func()
+
+
+def test_boxing_without_parent():
+    """Test unboxing of the event that does not have parent"""
+
+    @dpjit
+    def func() -> SyclEvent:
+        event = SyclEvent()
+        return event
+
+    e: SyclEvent = func()
+    ref_cnt = sys.getrefcount(e)
+
+    assert isinstance(e, SyclEvent)
+    assert ref_cnt == 2
diff --git a/numba_dpex/tests/core/types/DpctlSyclEvent/test_models.py b/numba_dpex/tests/core/types/DpctlSyclEvent/test_models.py
new file mode 100644
index 0000000000..6d60aa13e9
--- /dev/null
+++ b/numba_dpex/tests/core/types/DpctlSyclEvent/test_models.py
@@ -0,0 +1,31 @@
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpctl
+from numba import types
+from numba.core.datamodel import default_manager, models
+
+from numba_dpex.core.datamodel.models import (
+    SyclEventModel,
+    dpex_data_model_manager,
+)
+from numba_dpex.core.types.dpctl_types import DpctlSyclEvent
+
+
+def test_model_for_DpctlSyclEvent():
+    """Test the data model for DpctlSyclEvent that is registered with numba's
+    default data model manager.
+    """
+    sycl_event = DpctlSyclEvent()
+    default_model = default_manager.lookup(sycl_event)
+    assert isinstance(default_model, SyclEventModel)
+
+
+def test_sycl_event_Model():
+    """Test for sycl_event_Model.
+
+    It is a subclass of models.StructModel and models.ArrayModel.
+    """
+
+    assert issubclass(SyclEventModel, models.StructModel)
diff --git a/numba_dpex/tests/core/types/DpctlSyclEvent/test_overloads.py b/numba_dpex/tests/core/types/DpctlSyclEvent/test_overloads.py
new file mode 100644
index 0000000000..169a467b6e
--- /dev/null
+++ b/numba_dpex/tests/core/types/DpctlSyclEvent/test_overloads.py
@@ -0,0 +1,20 @@
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpctl
+
+from numba_dpex import dpjit
+
+
+@dpjit
+def wait_call(a):
+    a.wait()
+    return None
+
+
+def test_wait_DpctlSyclEvent():
+    """Test the dpctl.SyclEvent.wait() call overload."""
+
+    e = dpctl.SyclEvent()
+    wait_call(e)
diff --git a/numba_dpex/tests/core/types/DpctlSyclQueue/__init__.py b/numba_dpex/tests/core/types/DpctlSyclQueue/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/core/types/DpctlSyclQueue/__init__.py
+++ b/numba_dpex/tests/core/types/DpctlSyclQueue/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/types/DpctlSyclQueue/test_box.py b/numba_dpex/tests/core/types/DpctlSyclQueue/test_box.py
new file mode 100644
index 0000000000..26d59ee8b2
--- /dev/null
+++ b/numba_dpex/tests/core/types/DpctlSyclQueue/test_box.py
@@ -0,0 +1,27 @@
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Tests for boxing for dpctl.SyclQueue
+"""
+
+import dpnp
+import pytest
+from dpctl import SyclQueue
+
+from numba_dpex import dpjit
+
+
+def test_boxing_without_parent():
+    """Test unboxing of the queue that does not have parent"""
+
+    @dpjit
+    def func() -> SyclQueue:
+        arr = dpnp.empty(10)
+        queue = arr.sycl_queue
+        return queue
+
+    q: SyclQueue = func()
+
+    assert len(q.sycl_device.filter_string) > 0
diff --git a/numba_dpex/tests/core/types/DpctlSyclQueue/test_box_unbox.py b/numba_dpex/tests/core/types/DpctlSyclQueue/test_box_unbox.py
deleted file mode 100644
index 133b142ee1..0000000000
--- a/numba_dpex/tests/core/types/DpctlSyclQueue/test_box_unbox.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""
-Tests for boxing and unboxing of types supported inside dpjit
-"""
-
-import dpctl
-
-from numba_dpex import dpjit
-
-
-def test_boxing_unboxing():
-    """Tests basic boxing and unboxing of a dpctl.SyclQueue object.
-
-    Checks if we can pass in and return a dpctl.SyclQueue object to and
-    from a dpjit decorated function.
-    """
-
-    @dpjit
-    def func(a):
-        return a
-
-    q = dpctl.SyclQueue()
-    o = func(q)
-    assert id(o) == id(q)
diff --git a/numba_dpex/tests/core/types/DpctlSyclQueue/test_queue_ref_attr.py b/numba_dpex/tests/core/types/DpctlSyclQueue/test_queue_ref_attr.py
index 57cd6329b6..acbe25247c 100644
--- a/numba_dpex/tests/core/types/DpctlSyclQueue/test_queue_ref_attr.py
+++ b/numba_dpex/tests/core/types/DpctlSyclQueue/test_queue_ref_attr.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -8,9 +8,10 @@
 from numba.extending import intrinsic
 
 from numba_dpex import dpjit
+from numba_dpex.core.targets.dpjit_target import DPEX_TARGET_NAME
 
 
-@intrinsic
+@intrinsic(target=DPEX_TARGET_NAME)
 def are_queues_equal(typingctx, ty_queue1, ty_queue2):
     """Calls dpctl's libsyclinterface's DPCTLQueue_AreEq to see if two
     dpctl.SyclQueue objects point to the same sycl queue.
@@ -31,16 +32,21 @@ def are_queues_equal(typingctx, ty_queue1, ty_queue2):
 
     # defines the custom code generation
     def codegen(context, builder, sig, args):
+        q1 = cgutils.create_struct_proxy(ty_queue1)(
+            context, builder, value=args[0]
+        )
+        q2 = cgutils.create_struct_proxy(ty_queue2)(
+            context, builder, value=args[1]
+        )
+
         fnty = llvmir.FunctionType(
             cgutils.bool_t, [cgutils.voidptr_t, cgutils.voidptr_t]
         )
         fn = cgutils.get_or_insert_function(
             builder.module, fnty, "DPCTLQueue_AreEq"
         )
-        qref1 = builder.extract_value(args[0], 1)
-        qref2 = builder.extract_value(args[1], 1)
 
-        ret = builder.call(fn, [qref1, qref2])
+        ret = builder.call(fn, [q1.queue_ref, q2.queue_ref])
 
         return ret
 
diff --git a/numba_dpex/tests/core/types/DpnpNdArray/__init__.py b/numba_dpex/tests/core/types/DpnpNdArray/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/core/types/DpnpNdArray/__init__.py
+++ b/numba_dpex/tests/core/types/DpnpNdArray/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/types/DpnpNdArray/test_boxing_unboxing.py b/numba_dpex/tests/core/types/DpnpNdArray/test_boxing_unboxing.py
index b7ca7f13e0..e44f5f96d6 100644
--- a/numba_dpex/tests/core/types/DpnpNdArray/test_boxing_unboxing.py
+++ b/numba_dpex/tests/core/types/DpnpNdArray/test_boxing_unboxing.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -32,6 +32,8 @@ def func(a):
     assert a.device == b.device
     assert a.strides == b.strides
     assert a.dtype == b.dtype
+    # To ensure we are returning the original array when boxing
+    assert id(a) == id(b)
 
 
 def test_stride_calc_at_unboxing():
diff --git a/numba_dpex/tests/core/types/DpnpNdArray/test_bugs.py b/numba_dpex/tests/core/types/DpnpNdArray/test_bugs.py
index 2b4055b96b..af7baaf55d 100644
--- a/numba_dpex/tests/core/types/DpnpNdArray/test_bugs.py
+++ b/numba_dpex/tests/core/types/DpnpNdArray/test_bugs.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/types/DpnpNdArray/test_dpnp_ndarray_type.py b/numba_dpex/tests/core/types/DpnpNdArray/test_dpnp_ndarray_type.py
index 3eff0492aa..d0082e80e8 100644
--- a/numba_dpex/tests/core/types/DpnpNdArray/test_dpnp_ndarray_type.py
+++ b/numba_dpex/tests/core/types/DpnpNdArray/test_dpnp_ndarray_type.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/types/DpnpNdArray/test_models.py b/numba_dpex/tests/core/types/DpnpNdArray/test_models.py
deleted file mode 100644
index 79a1645950..0000000000
--- a/numba_dpex/tests/core/types/DpnpNdArray/test_models.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
-#
-# SPDX-License-Identifier: Apache-2.0
-
-from numba import types
-from numba.core.datamodel import default_manager, models
-
-from numba_dpex.core.datamodel.models import (
-    DpnpNdArrayModel,
-    USMArrayModel,
-    dpex_data_model_manager,
-)
-from numba_dpex.core.types.dpnp_ndarray_type import DpnpNdArray
-
-
-def test_model_for_DpnpNdArray():
-    """Test the datamodel for DpnpNdArray that is registered with numba's
-    default datamodel manager and numba_dpex's kernel data model manager.
-    """
-    dpnp_ndarray = DpnpNdArray(ndim=1, dtype=types.float64, layout="C")
-    model = dpex_data_model_manager.lookup(dpnp_ndarray)
-    assert isinstance(model, USMArrayModel)
-    default_model = default_manager.lookup(dpnp_ndarray)
-    assert isinstance(default_model, DpnpNdArrayModel)
-
-
-def test_dpnp_ndarray_Model():
-    """Test for dpnp_ndarray_Model.
-
-    It is a subclass of models.StructModel and models.ArrayModel.
-    """
-
-    assert issubclass(DpnpNdArrayModel, models.StructModel)
diff --git a/numba_dpex/tests/core/types/USMNdArray/__init__.py b/numba_dpex/tests/core/types/USMNdArray/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/core/types/USMNdArray/__init__.py
+++ b/numba_dpex/tests/core/types/USMNdArray/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/types/USMNdArray/test_array_creation_errors.py b/numba_dpex/tests/core/types/USMNdArray/test_array_creation_errors.py
index 343ad19906..335cfadcc9 100644
--- a/numba_dpex/tests/core/types/USMNdArray/test_array_creation_errors.py
+++ b/numba_dpex/tests/core/types/USMNdArray/test_array_creation_errors.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
 import dpctl
 import pytest
 
diff --git a/numba_dpex/tests/core/types/USMNdArray/test_usm_ndarray_creation.py b/numba_dpex/tests/core/types/USMNdArray/test_usm_ndarray_creation.py
index a6673b5a64..5c0da34b83 100644
--- a/numba_dpex/tests/core/types/USMNdArray/test_usm_ndarray_creation.py
+++ b/numba_dpex/tests/core/types/USMNdArray/test_usm_ndarray_creation.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
 import dpctl
 import pytest
 
diff --git a/numba_dpex/tests/core/types/USMNdArray/test_usm_ndarray_type.py b/numba_dpex/tests/core/types/USMNdArray/test_usm_ndarray_type.py
index c0c984317a..e20483e439 100644
--- a/numba_dpex/tests/core/types/USMNdArray/test_usm_ndarray_type.py
+++ b/numba_dpex/tests/core/types/USMNdArray/test_usm_ndarray_type.py
@@ -1,4 +1,4 @@
-# Copyright 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/types/__init__.py b/numba_dpex/tests/core/types/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/core/types/__init__.py
+++ b/numba_dpex/tests/core/types/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/core/types/range_types/__init__.py b/numba_dpex/tests/core/types/range_types/__init__.py
new file mode 100644
index 0000000000..1d329c9611
--- /dev/null
+++ b/numba_dpex/tests/core/types/range_types/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/tests/core/types/range_types/test_constructor_overloads.py b/numba_dpex/tests/core/types/range_types/test_constructor_overloads.py
new file mode 100644
index 0000000000..c9f6118078
--- /dev/null
+++ b/numba_dpex/tests/core/types/range_types/test_constructor_overloads.py
@@ -0,0 +1,35 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import pytest
+
+from numba_dpex import NdRange, Range, dpjit
+
+ranges = [(10,), (10, 10), (10, 10, 10)]
+
+
+@pytest.mark.parametrize("r", ranges)
+def test_range_ctor(r):
+    @dpjit
+    def _tester(r):
+        return Range(*r)
+
+    r_expected = Range(*r)
+    r_out = _tester(r)
+
+    assert r_out == r_expected
+
+
+@pytest.mark.parametrize("r", ranges)
+def test_ndrange_unbox_box(r):
+    @dpjit
+    def _tester(r):
+        gr = lr = Range(*r)
+        return NdRange(gr, lr)
+
+    gr = lr = Range(*r)
+    r_expected = NdRange(gr, lr)
+    r_out = _tester(r)
+
+    assert r_out == r_expected
diff --git a/numba_dpex/tests/core/types/range_types/test_data_model.py b/numba_dpex/tests/core/types/range_types/test_data_model.py
new file mode 100644
index 0000000000..9dd4aad5c7
--- /dev/null
+++ b/numba_dpex/tests/core/types/range_types/test_data_model.py
@@ -0,0 +1,83 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import pytest
+from numba.core.datamodel import default_manager
+from numba.core.registry import cpu_target
+
+from numba_dpex.core.datamodel.models import (
+    NdRangeModel,
+    RangeModel,
+    dpex_data_model_manager,
+)
+from numba_dpex.core.descriptor import dpex_kernel_target
+from numba_dpex.core.types.range_types import NdRangeType, RangeType
+
+rfields = ["ndim", "dim0", "dim1", "dim2"]
+ndrfields = ["ndim", "gdim0", "gdim1", "gdim2", "ldim0", "ldim1", "ldim2"]
+range_tys = [RangeType, NdRangeType]
+
+
+def test_datamodel_registration():
+    """Test the datamodel for RangeType and NdRangeType are found in numba's
+    default datamodel manager but not in numba_dpex's kernel data model manager.
+    """
+    range_ty = RangeType(ndim=1)
+    ndrange_ty = NdRangeType(ndim=1)
+
+    with pytest.raises(KeyError):
+        dpex_data_model_manager.lookup(range_ty)
+        dpex_data_model_manager.lookup(ndrange_ty)
+
+    default_range_model = default_manager.lookup(range_ty)
+    default_ndrange_model = default_manager.lookup(ndrange_ty)
+
+    assert isinstance(default_range_model, RangeModel)
+    assert isinstance(default_ndrange_model, NdRangeModel)
+
+
+@pytest.mark.parametrize("field", rfields)
+def test_range_model_fields(field):
+    """Tests that the expected fields are found in the data model for
+    RangeType
+    """
+    range_ty = RangeType(ndim=1)
+    dm = default_manager.lookup(range_ty)
+    try:
+        dm.get_field_position(field)
+    except:
+        pytest.fail(f"Expected field {field} not present in RangeModel")
+
+
+@pytest.mark.parametrize("field", ndrfields)
+def test_ndrange_model_fields(field):
+    """Tests that the expected fields are found in the data model for
+    NdRangeType
+    """
+    ndrange_ty = NdRangeType(ndim=1)
+    dm = default_manager.lookup(ndrange_ty)
+    try:
+        dm.get_field_position(field)
+    except:
+        pytest.fail(f"Expected field {field} not present in NdRangeModel")
+
+
+@pytest.mark.parametrize("range_type", range_tys)
+def test_flattened_member_count(range_type):
+    """Test that the number of flattened member count matches the number of
+    flattened args generated by the CpuTarget's ArgPacker.
+    """
+
+    cputargetctx = cpu_target.target_context
+    kerneltargetctx = dpex_kernel_target.target_context
+    dpex_dmm = kerneltargetctx.data_model_manager
+
+    for ndim in range(1, 3):
+        dty = range_type(ndim)
+        argty_tuple = tuple([dty])
+        datamodel = dpex_dmm.lookup(dty)
+        num_flattened_args = datamodel.flattened_field_count
+        ap = cputargetctx.get_arg_packer(argty_tuple)
+
+        assert num_flattened_args == len(ap._be_args)
diff --git a/numba_dpex/tests/core/types/test_array_models.py b/numba_dpex/tests/core/types/test_array_models.py
new file mode 100644
index 0000000000..1bcbb7a59d
--- /dev/null
+++ b/numba_dpex/tests/core/types/test_array_models.py
@@ -0,0 +1,62 @@
+# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import pytest
+from numba import types
+from numba.core.datamodel import default_manager, models
+from numba.core.registry import cpu_target
+
+from numba_dpex.core.datamodel.models import (
+    USMArrayDeviceModel,
+    USMArrayHostModel,
+    dpex_data_model_manager,
+)
+from numba_dpex.core.types.dpnp_ndarray_type import DpnpNdArray, USMNdArray
+
+
+@pytest.fixture(
+    params=[
+        array(ndim=ndim, dtype=types.float32, layout="C")
+        for array in [DpnpNdArray, USMNdArray]
+        for ndim in range(4)
+    ]
+)
+def nd_array(request):
+    return request.param
+
+
+def test_model_for_array(nd_array):
+    """Test the datamodel for DpnpNdArray that is registered with numba's
+    default datamodel manager and numba_dpex's kernel data model manager.
+    """
+    device_model = dpex_data_model_manager.lookup(nd_array)
+    assert isinstance(device_model, USMArrayDeviceModel)
+    host_model = default_manager.lookup(nd_array)
+    assert isinstance(host_model, USMArrayHostModel)
+
+
+def test_dpnp_usm_ndarray_model():
+    """Test for dpctl.tensor.usm_ndarray model.
+
+    It is a subclass of models.StructModel and models.ArrayModel.
+    """
+
+    assert issubclass(USMArrayHostModel, models.StructModel)
+    assert issubclass(USMArrayDeviceModel, models.StructModel)
+
+
+def test_flattened_member_count(nd_array):
+    """Test that the number of flattened member count matches the number of
+    flattened args generated by the CpuTarget's ArgPacker.
+    """
+
+    cputargetctx = cpu_target.target_context
+    dpex_dmm = cputargetctx.data_model_manager
+
+    argty_tuple = tuple([nd_array])
+    datamodel = dpex_dmm.lookup(nd_array)
+    num_flattened_args = datamodel.flattened_field_count
+    ap = cputargetctx.get_arg_packer(argty_tuple)
+
+    assert num_flattened_args == len(ap._be_args)
diff --git a/numba_dpex/tests/core/types/test_box_unbox.py b/numba_dpex/tests/core/types/test_box_unbox.py
new file mode 100644
index 0000000000..a792414f94
--- /dev/null
+++ b/numba_dpex/tests/core/types/test_box_unbox.py
@@ -0,0 +1,144 @@
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Tests for boxing and unboxing of types supported inside dpjit
+"""
+
+import functools
+import sys
+
+import dpctl
+import dpctl.tensor as dpt
+import dpnp
+import numpy
+import pytest
+
+import numba_dpex as dpx
+
+constructors_with_parent = [
+    dpctl.SyclEvent,
+    dpctl.SyclQueue,
+    # numpy test is actually not related to the dpex, but helps make sure we are
+    # doing it in the right way. Testing tests =)
+    lambda: numpy.empty(10, dtype=numpy.float32),
+    lambda: dpnp.empty(10, dtype=dpnp.float32),
+    lambda: dpt.empty(10, dtype=dpt.float32),
+]
+
+ranges = [(10,), (10, 10), (10, 10, 10)]
+
+constructors_without_parent = [
+    functools.partial(dpx.Range, *r) for r in ranges
+] + [
+    functools.partial(dpx.NdRange, dpx.Range(*r), dpx.Range(*r)) for r in ranges
+]
+
+
+@pytest.fixture(params=constructors_with_parent + constructors_without_parent)
+def constructor(request):
+    return request.param
+
+
+@pytest.fixture(params=constructors_with_parent)
+def constructor_wp(request):
+    return request.param
+
+
+@pytest.fixture(params=constructors_without_parent)
+def constructor_np(request):
+    return request.param
+
+
+@dpx.dpjit
+def unbox_box(a):
+    return a
+
+
+@dpx.dpjit
+def unbox(a):
+    return None
+
+
+def test_unboxing_boxing_with_parent_and_return(constructor_wp):
+    """Tests basic boxing and unboxing of an object.
+
+    Checks if we can pass in and return the object to and from a dpjit decorated
+    function. Checks if we have expected count of references.
+    """
+
+    e = constructor_wp()
+
+    ref_before = sys.getrefcount(e)
+    o = unbox_box(e)
+    ref_after = sys.getrefcount(e)
+
+    assert ref_before + 1 == ref_after  # we have 'o' referencing same object
+    assert id(o) == id(e)
+
+
+def test_unboxing_boxing_with_return(constructor_np):
+    """Tests basic boxing and unboxing of an object.
+
+    Checks if we can pass in and return the object to and from a dpjit decorated
+    function. Checks if we have expected count of references.
+    """
+
+    e = constructor_np()
+
+    ref_before = sys.getrefcount(e)
+    o = unbox_box(e)
+    ref_after = sys.getrefcount(e)
+
+    assert id(o) != id(e)
+    assert ref_before == ref_after  # we have 'o' referencing same object
+
+
+def test_unboxing_boxing_with_same_assign(constructor):
+    """Tests basic boxing and unboxing of an object.
+
+    Checks if we can pass in and return the object to and from a dpjit decorated
+    function. Checks if we have expected count of references.
+    """
+
+    e = constructor()
+
+    ref_before = sys.getrefcount(e)
+    e = unbox_box(e)
+    ref_after = sys.getrefcount(e)
+
+    assert ref_before == ref_after
+
+
+def test_unboxing_boxing_without_return(constructor):
+    """Tests basic boxing and unboxing of an object.
+
+    Checks if we can pass in and return the object to and from a dpjit decorated
+    function without assigning it to new variable.
+    Checks if it does not affect reference count.
+    """
+
+    e = constructor()
+
+    ref_before = sys.getrefcount(e)
+    unbox_box(e)
+    ref_after = sys.getrefcount(e)
+
+    assert ref_before == ref_after
+
+
+def test_unboxing(constructor):
+    """Tests basic unboxing of an object.
+
+    Checks if we have expected amount of reference if we pass the object to a
+    dpjit decorated function.
+    """
+
+    e = constructor()
+
+    ref_before = sys.getrefcount(e)
+    unbox(e)
+    ref_after = sys.getrefcount(e)
+
+    assert ref_before == ref_after
diff --git a/numba_dpex/tests/debugging/__init__.py b/numba_dpex/tests/debugging/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/debugging/__init__.py
+++ b/numba_dpex/tests/debugging/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/debugging/common.py b/numba_dpex/tests/debugging/common.py
index a78b202e5d..4ffd0c2a26 100644
--- a/numba_dpex/tests/debugging/common.py
+++ b/numba_dpex/tests/debugging/common.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/debugging/conftest.py b/numba_dpex/tests/debugging/conftest.py
index 1776f5116e..2c8d857fcb 100644
--- a/numba_dpex/tests/debugging/conftest.py
+++ b/numba_dpex/tests/debugging/conftest.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/debugging/gdb.py b/numba_dpex/tests/debugging/gdb.py
index af040077bf..1b3b6f8951 100644
--- a/numba_dpex/tests/debugging/gdb.py
+++ b/numba_dpex/tests/debugging/gdb.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -9,7 +9,7 @@
 
 import pytest
 
-from numba_dpex import config
+from numba_dpex.core import config
 
 from .common import script_path
 
diff --git a/numba_dpex/tests/debugging/test_backtraces.py b/numba_dpex/tests/debugging/test_backtraces.py
index b38a8d8320..34676c0787 100644
--- a/numba_dpex/tests/debugging/test_backtraces.py
+++ b/numba_dpex/tests/debugging/test_backtraces.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -8,6 +8,7 @@
 
 https://www.sourceware.org/gdb/onlinedocs/gdb/Backtrace.html
 """
+import pytest
 
 from numba_dpex.tests._helper import skip_no_gdb
 
@@ -16,6 +17,7 @@
 pytestmark = skip_no_gdb
 
 
+@pytest.mark.xfail  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
 def test_backtrace(app):
     """Simple test for backtrace.
 
diff --git a/numba_dpex/tests/debugging/test_breakpoints.py b/numba_dpex/tests/debugging/test_breakpoints.py
index a8d97c6f7a..adf0d07473 100644
--- a/numba_dpex/tests/debugging/test_breakpoints.py
+++ b/numba_dpex/tests/debugging/test_breakpoints.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -57,6 +57,11 @@ def test_breakpoint_with_condition_by_function_argument(app, breakpoint, api):
     Test for https://github.com/numba/numba/issues/7415
     SAT-4449
     """
+    if api == "numba-dpex-kernel":
+        pytest.xfail(
+            "Wrong name for kernel api."
+        )  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
+
     variable_name = "param_a"
     variable_value = "3"
     condition = f"{variable_name} == {variable_value}"
@@ -73,17 +78,18 @@ def test_breakpoint_with_condition_by_function_argument(app, breakpoint, api):
     app.child.expect(rf"\$1 = {variable_value}")
 
 
+@pytest.mark.xfail  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
 @pytest.mark.parametrize(
     "breakpoint, script",
     [
         # location specified by file name and function name
-        # commands/break_file_func
+        # commands/break_file_func # noqa: E800
         ("simple_sum.py:data_parallel_sum", None),
         # location specified by function name
-        # commands/break_func
+        # commands/break_func # noqa: E800
         ("data_parallel_sum", "simple_sum.py"),
         # location specified by file name and nested function name
-        # commands/break_nested_func
+        # commands/break_nested_func # noqa: E800
         ("simple_dpex_func.py:func_sum", None),
     ],
 )
@@ -95,7 +101,7 @@ def test_breakpoint_common(app, breakpoint, script):
 @pytest.mark.parametrize(
     "breakpoint, variable_name, variable_value",
     [
-        # commands/break_conditional
+        # commands/break_conditional # noqa: E800
         (f"{simple_sum_condition_breakpoint} if i == 1", "i", "1"),
     ],
 )
diff --git a/numba_dpex/tests/debugging/test_common.py b/numba_dpex/tests/debugging/test_common.py
index a4bc2a8e5d..45ba586fcd 100644
--- a/numba_dpex/tests/debugging/test_common.py
+++ b/numba_dpex/tests/debugging/test_common.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -50,4 +50,12 @@ def test_breakpoint_by_function(file_name, function, expected):
 def test_setup_breakpoint(
     app, breakpoint, script, expected_location, expected_line
 ):
+    if (
+        breakpoint == "simple_sum.py:data_parallel_sum"
+        or breakpoint == "data_parallel_sum"
+    ):
+        pytest.xfail(
+            "Expected failures for these files."
+        )  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
+
     setup_breakpoint(app, breakpoint, script, expected_location, expected_line)
diff --git a/numba_dpex/tests/debugging/test_info.py b/numba_dpex/tests/debugging/test_info.py
index 8d892439df..297c2653ca 100644
--- a/numba_dpex/tests/debugging/test_info.py
+++ b/numba_dpex/tests/debugging/test_info.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -70,6 +70,13 @@ def test_info_args(
     Issue: https://github.com/numba/numba/issues/7414
     Fix: https://github.com/numba/numba/pull/7177
     """
+    if (
+        script == "simple_dpex_func.py"
+        or script == "side-by-side.py --api=numba-dpex-kernel"
+    ):
+        pytest.xfail(
+            "Expected Failure for these files."
+        )  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
 
     setup_breakpoint(app, breakpoint, script, expected_line=expected_line)
 
@@ -90,6 +97,7 @@ def test_info_args(
     app.child.expect(expected_whatis)
 
 
+@pytest.mark.xfail  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
 @skip_no_numba056
 def test_info_functions(app):
     expected_line = r"13\s+i = dpex.get_global_id\(0\)"
@@ -145,7 +153,7 @@ def side_by_side_2_info_locals_case(api):
 @pytest.mark.parametrize(
     "env, breakpoint, script, expected_line, expected_info_locals, expected_info",
     [
-        (
+        pytest.param(
             {"NUMBA_OPT": 0},
             "sum_local_vars.py:16",
             "sum_local_vars.py",
@@ -175,6 +183,7 @@ def side_by_side_2_info_locals_case(api):
                     r"type = float64",
                 ),
             ),
+            marks=pytest.mark.xfail,
         ),
         # FIXME: NUMBA_OPT=1 will not able to stop at breakpoint
         pytest.param(
@@ -184,36 +193,42 @@ def side_by_side_2_info_locals_case(api):
             r"16\s+c\[i\] = l1 \+ l2",
             ("No locals.",),
             (),
-            marks=pytest.mark.xfail,
+            marks=pytest.mark.xfail,  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
         ),
-        (
+        pytest.param(
             {"NUMBA_EXTEND_VARIABLE_LIFETIMES": 1},
             "side-by-side.py:18",
             "side-by-side.py --api=numba-dpex-kernel",
             None,
             (r"param_c = 0", r"param_d = 0", r"result = 10"),
             (),
+            marks=pytest.mark.xfail,  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
         ),
-        (
+        pytest.param(
             {"NUMBA_EXTEND_VARIABLE_LIFETIMES": 0},
             "side-by-side.py:18",
             "side-by-side.py --api=numba-dpex-kernel",
             None,
             (r"param_c = 0", r"param_d = 0", r"result = 10"),
             (),
+            marks=pytest.mark.xfail,  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
         ),
         side_by_side_info_locals_case("numba"),
         pytest.param(
             *side_by_side_info_locals_case("numba-dpex-kernel"),
             marks=[
-                pytest.mark.xfail(reason="dpex isn't stoping with condition")
+                pytest.mark.xfail(
+                    reason="dpex isn't stoping with condition"
+                )  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
             ],
         ),
         side_by_side_2_info_locals_case("numba"),
         pytest.param(
             *side_by_side_2_info_locals_case("numba-dpex-kernel"),
             marks=[
-                pytest.mark.xfail(reason="dpex isn't stoping with condition")
+                pytest.mark.xfail(
+                    reason="dpex isn't stoping with condition"
+                )  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
             ],
         ),
     ],
@@ -277,7 +292,9 @@ def side_by_side_2_print_array_element_case(api):
         pytest.param(
             *side_by_side_2_print_array_element_case("numba-dpex-kernel"),
             marks=[
-                pytest.mark.xfail(reason="dpex isn't stoping with condition")
+                pytest.mark.xfail(
+                    reason="dpex isn't stoping with condition"
+                )  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
             ],
         ),
     ],
@@ -294,6 +311,8 @@ def test_print_array_element(app, breakpoint, script, expected_info):
         app.child.expect(expected_print)
 
 
+# FIXME: crashes test execution
+@pytest.mark.skip  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
 def side_by_side_2_assignment_to_variable_case(api):
     return (
         "side-by-side-2.py:19 if param_a == 5",
@@ -316,7 +335,9 @@ def side_by_side_2_assignment_to_variable_case(api):
         pytest.param(
             *side_by_side_2_assignment_to_variable_case("numba-dpex-kernel"),
             marks=[
-                pytest.mark.xfail(reason="dpex isn't stoping with condition")
+                pytest.mark.xfail(
+                    reason="dpex isn't stoping with condition"
+                )  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
             ],
         ),
     ],
diff --git a/numba_dpex/tests/debugging/test_side_by_side.py b/numba_dpex/tests/debugging/test_side_by_side.py
index 0d7abe2125..330a640e00 100644
--- a/numba_dpex/tests/debugging/test_side_by_side.py
+++ b/numba_dpex/tests/debugging/test_side_by_side.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -18,6 +18,11 @@
 def test_breakpoint_row_number(app, api):
     """Test for checking numba and numba-dpex debugging side-by-side."""
 
+    if api == "numba-dpex-kernel":
+        pytest.xfail(
+            "Wrong name for kernel api."
+        )  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
+
     app.breakpoint("side-by-side.py:15")
     app.run("side-by-side.py --api={api}".format(api=api))
 
diff --git a/numba_dpex/tests/debugging/test_stepping.py b/numba_dpex/tests/debugging/test_stepping.py
index 07802679d8..d33ca28733 100644
--- a/numba_dpex/tests/debugging/test_stepping.py
+++ b/numba_dpex/tests/debugging/test_stepping.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -9,6 +9,8 @@
 https://www.sourceware.org/gdb/onlinedocs/gdb/Continuing-and-Stepping.html
 """
 
+import pytest
+
 from numba_dpex.tests._helper import skip_no_gdb
 
 from .common import setup_breakpoint
@@ -17,6 +19,7 @@
 
 
 # commands/next
+@pytest.mark.xfail  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
 def test_next(app):
     setup_breakpoint(
         app,
@@ -31,6 +34,7 @@ def test_next(app):
 
 
 # commands/step_dpex_func
+@pytest.mark.xfail  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
 def test_step(app):
     setup_breakpoint(
         app,
@@ -46,6 +50,7 @@ def test_step(app):
 
 
 # commands/stepi
+@pytest.mark.xfail  # TODO: https://github.com/IntelPython/numba-dpex/issues/1216
 def test_stepi(app):
     setup_breakpoint(
         app,
diff --git a/numba_dpex/tests/dpjit_tests/__init__.py b/numba_dpex/tests/dpjit_tests/__init__.py
index 5ca1120d9e..02650dffb8 100644
--- a/numba_dpex/tests/dpjit_tests/__init__.py
+++ b/numba_dpex/tests/dpjit_tests/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/dpnp/__init__.py b/numba_dpex/tests/dpjit_tests/dpnp/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/dpjit_tests/dpnp/__init__.py
+++ b/numba_dpex/tests/dpjit_tests/dpnp/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_empty.py b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_empty.py
index 1a70aee498..4808bf3f9b 100644
--- a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_empty.py
+++ b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_empty.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -139,3 +139,33 @@ def func(shape, queue):
     with pytest.raises(errors.TypingError):
         queue = dpctl.SyclQueue()
         func(10, queue)
+
+
+@pytest.mark.xfail(reason="dpjit allocates new dpctl.SyclQueue on boxing")
+# TODO: remove test_dpnp_empty_with_dpjit_queue_temp once pass.
+def test_dpnp_empty_with_dpjit_queue():
+    """Test if dpnp array can be created with a queue from another array"""
+
+    @dpjit
+    def func(a):
+        queue = a.sycl_queue
+        return dpnp.empty(10, sycl_queue=queue)
+
+    a = dpnp.empty(10)
+    b = func(a)
+
+    assert id(a.sycl_queue) == id(b.sycl_queue)
+
+
+def test_dpnp_empty_with_dpjit_queue_temp():
+    """Test if dpnp array can be created with a queue from another array"""
+
+    @dpjit
+    def func(a):
+        queue = a.sycl_queue
+        return dpnp.empty(10, sycl_queue=queue)
+
+    a = dpnp.empty(10)
+    b = func(a)
+
+    assert a.sycl_queue == b.sycl_queue
diff --git a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_empty_like.py b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_empty_like.py
index 4d7e10547a..e1cc0b209b 100644
--- a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_empty_like.py
+++ b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_empty_like.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_full.py b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_full.py
index 298aeafbb0..cc15906a8d 100644
--- a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_full.py
+++ b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_full.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -118,7 +118,6 @@ def func(shape, fill_value):
             "Returned queue does not have the same queue as cached against the device."
         )
 
-    # dummy = dpnp.full(shape, fill_value, dtype=dtype)
     # dpnp can't cast 4294967295 into int32 and so on,
     # but we can, also numpy can, so we are using numpy here
     dummy = numpy.full(shape, fill_value, dtype=dtype)
@@ -164,7 +163,6 @@ def func(shape, fill_value, queue):
             "Returned queue does not have the same queue as the one passed to the dpnp function."
         )
 
-    # dummy = dpnp.full(shape, fill_value, dtype=dtype)
     # dpnp can't cast 4294967295 into int32 and so on,
     # but we can, also numpy can, so we are using numpy here
     dummy = numpy.full(shape, fill_value, dtype=dtype)
diff --git a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_full_like.py b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_full_like.py
index fb998ebe57..b466b92575 100644
--- a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_full_like.py
+++ b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_full_like.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -117,7 +117,6 @@ def func(x, fill_value):
             "Returned queue does not have the same queue as cached against the device."
         )
 
-    # dummy = dpnp.full_like(a, fill_value, dtype=dtype)
     # dpnp can't cast 4294967295 into int32 and so on,
     # but we can, also numpy can, so we are using numpy here
     dummy = numpy.full_like(a.asnumpy(), fill_value, dtype=dtype)
@@ -161,7 +160,6 @@ def func(x, fill_value, queue):
     assert c.sycl_queue == a.sycl_queue
     assert c.sycl_queue == queue
 
-    # dummy = dpnp.full_like(a, fill_value, dtype=dtype)
     # dpnp can't cast 4294967295 into int32 and so on,
     # but we can, also numpy can, so we are using numpy here
     dummy = numpy.full_like(a.asnumpy(), fill_value, dtype=dtype)
diff --git a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_ones.py b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_ones.py
index 0370dbbedc..07706d34bf 100644
--- a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_ones.py
+++ b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_ones.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_ones_like.py b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_ones_like.py
index 174af5ce6b..ca5070c144 100644
--- a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_ones_like.py
+++ b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_ones_like.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_zeros.py b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_zeros.py
index fdf2d36131..e301587114 100644
--- a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_zeros.py
+++ b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_zeros.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_zeros_like.py b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_zeros_like.py
index 1053394ca2..3cb15cc315 100644
--- a/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_zeros_like.py
+++ b/numba_dpex/tests/dpjit_tests/dpnp/test_dpnp_zeros_like.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/dpnp/test_target_specific_overload.py b/numba_dpex/tests/dpjit_tests/dpnp/test_target_specific_overload.py
new file mode 100644
index 0000000000..c7433aa792
--- /dev/null
+++ b/numba_dpex/tests/dpjit_tests/dpnp/test_target_specific_overload.py
@@ -0,0 +1,27 @@
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Tests if dpnp dpex specific overloads are not available at numba njit.
+"""
+
+import dpnp
+import pytest
+from numba import njit
+from numba.core import errors
+
+from numba_dpex import dpjit
+
+
+@pytest.mark.parametrize("func", [dpnp.empty, dpnp.ones, dpnp.zeros])
+def test_dpnp_dpex_target(func):
+    def dpnp_func():
+        func(10)
+
+    dpnp_func_njit = njit(dpnp_func)
+    dpnp_func_dpjit = dpjit(dpnp_func)
+
+    dpnp_func_dpjit()
+    with pytest.raises(errors.TypingError):
+        dpnp_func_njit()
diff --git a/numba_dpex/tests/dpjit_tests/parfors/__init__.py b/numba_dpex/tests/dpjit_tests/parfors/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/dpjit_tests/parfors/__init__.py
+++ b/numba_dpex/tests/dpjit_tests/parfors/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/parfors/prange/__init__.py b/numba_dpex/tests/dpjit_tests/parfors/prange/__init__.py
index e69de29bb2..ee20629a35 100644
--- a/numba_dpex/tests/dpjit_tests/parfors/prange/__init__.py
+++ b/numba_dpex/tests/dpjit_tests/parfors/prange/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/tests/dpjit_tests/parfors/prange/test_pairwise_distance.py b/numba_dpex/tests/dpjit_tests/parfors/prange/test_pairwise_distance.py
index 834cae54e3..c6677dcd5c 100644
--- a/numba_dpex/tests/dpjit_tests/parfors/prange/test_pairwise_distance.py
+++ b/numba_dpex/tests/dpjit_tests/parfors/prange/test_pairwise_distance.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/parfors/test_builtin_ops.py b/numba_dpex/tests/dpjit_tests/parfors/test_builtin_ops.py
index 9434f35ba4..8777d0e241 100644
--- a/numba_dpex/tests/dpjit_tests/parfors/test_builtin_ops.py
+++ b/numba_dpex/tests/dpjit_tests/parfors/test_builtin_ops.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -39,7 +39,7 @@ def parfor_exponent(a, b):
 dtypes = get_all_dtypes(
     no_bool=True, no_float16=True, no_none=True, no_complex=True
 )
-usm_types = ["device"]
+usm_types = ["device", "host", "shared"]
 funcs = [
     parfor_add,
     parfor_sub,
@@ -63,44 +63,7 @@ def parfor_floor(a, b):
 @pytest.mark.parametrize("dtype", dtypes)
 @pytest.mark.parametrize("usm_type", usm_types)
 @pytest.mark.parametrize("func", funcs)
-def test_built_in_operators1(shape, dtype, usm_type, func):
-    queue = dpctl.SyclQueue()
-    a = dpnp.zeros(
-        shape=shape, dtype=dtype, usm_type=usm_type, sycl_queue=queue
-    )
-    b = dpnp.ones(shape=shape, dtype=dtype, usm_type=usm_type, sycl_queue=queue)
-    try:
-        op = dpjit(func)
-        c = op(a, b)
-        del op
-    except Exception:
-        pytest.fail("Failed to compile.")
-
-    if len(c.shape) == 1:
-        assert c.shape[0] == shape
-    else:
-        assert c.shape == shape
-
-    if func != parfor_divide:
-        assert c.dtype == dtype
-    assert c.usm_type == usm_type
-
-    assert c.sycl_device.filter_string == queue.sycl_device.filter_string
-
-    expected = dpnp.asnumpy(func(a, b))
-    nc = dpnp.asnumpy(c)
-
-    numpy.allclose(nc, expected)
-
-
-usm_types = ["host", "shared"]
-
-
-@pytest.mark.parametrize("shape", shapes)
-@pytest.mark.parametrize("dtype", dtypes)
-@pytest.mark.parametrize("usm_type", usm_types)
-@pytest.mark.parametrize("func", funcs)
-def test_built_in_operators2(shape, dtype, usm_type, func):
+def test_built_in_operators(shape, dtype, usm_type, func):
     queue = dpctl.SyclQueue()
     a = dpnp.zeros(
         shape=shape, dtype=dtype, usm_type=usm_type, sycl_queue=queue
diff --git a/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_bitwise_ops.py b/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_bitwise_ops.py
index 6740a5da00..ab10c83e2f 100644
--- a/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_bitwise_ops.py
+++ b/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_bitwise_ops.py
@@ -1,8 +1,7 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-
 import dpctl.tensor as dpt
 import dpnp
 import numpy as np
diff --git a/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_logic_ops.py b/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_logic_ops.py
index 34c1e993c6..9263cc83b4 100644
--- a/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_logic_ops.py
+++ b/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_logic_ops.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -78,7 +78,6 @@ def f(a, b):
     )
 
 
-@pytest.mark.xfail
 def test_unary_ops(unary_op, input_arrays):
     a = input_arrays[0]
     uop = getattr(dpnp, unary_op)
diff --git a/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_transcedental_functions.py b/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_transcedental_functions.py
index 7882f6c5d7..71b6e02d30 100644
--- a/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_transcedental_functions.py
+++ b/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_transcedental_functions.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_trigonometric_functions.py b/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_trigonometric_functions.py
index 9eb4c771d1..f5fcc1aa8d 100644
--- a/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_trigonometric_functions.py
+++ b/numba_dpex/tests/dpjit_tests/parfors/test_dpnp_trigonometric_functions.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/test_dpex_target_overload_isolation.py b/numba_dpex/tests/dpjit_tests/test_dpex_target_overload_isolation.py
new file mode 100644
index 0000000000..19490054ee
--- /dev/null
+++ b/numba_dpex/tests/dpjit_tests/test_dpex_target_overload_isolation.py
@@ -0,0 +1,81 @@
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Tests if dpex target overloads are not available at numba.njit and only
+available at numba_dpex.dpjit.
+"""
+
+import pytest
+from numba import njit, types
+from numba.core import errors
+from numba.extending import intrinsic, overload
+
+from numba_dpex import dpjit
+from numba_dpex.core.targets.dpjit_target import DPEX_TARGET_NAME
+
+
+def foo():
+    return 1
+
+
+@overload(foo, target=DPEX_TARGET_NAME)
+def ol_foo():
+    return lambda: 1
+
+
+@intrinsic(target=DPEX_TARGET_NAME)
+def intrinsic_foo(
+    ty_context,
+):
+    """A numba "intrinsic" function to inject dpctl.SyclEvent constructor code.
+
+    Args:
+        ty_context (numba.core.typing.context.Context): The typing context
+            for the codegen.
+
+    Returns:
+        tuple(numba.core.typing.templates.Signature, function): A tuple of
+            numba function signature type and a function object.
+    """
+
+    sig = types.int32(types.void)
+
+    def codegen(context, builder, sig, args: list):
+        return context.get_constant(types.int32, 1)
+
+    return sig, codegen
+
+
+def bar():
+    return foo()
+
+
+def intrinsic_bar():
+    res = intrinsic_foo()
+    return res
+
+
+def test_dpex_overload_from_njit():
+    bar_njit = njit(bar)
+
+    with pytest.raises(errors.TypingError):
+        bar_njit()
+
+
+def test_dpex_overload_from_dpjit():
+    bar_dpjit = dpjit(bar)
+    bar_dpjit()
+
+
+def test_dpex_intrinsic_from_njit():
+    bar_njit = njit(intrinsic_bar)
+
+    with pytest.raises(errors.TypingError):
+        bar_njit()
+
+
+def test_dpex_intrinsic_from_dpjit():
+    bar_dpjit = dpjit(intrinsic_bar)
+    bar_dpjit()
diff --git a/numba_dpex/tests/dpjit_tests/test_dpjit_complex_arg_types.py b/numba_dpex/tests/dpjit_tests/test_dpjit_complex_arg_types.py
index 43714689c1..95ba5bbfb4 100644
--- a/numba_dpex/tests/dpjit_tests/test_dpjit_complex_arg_types.py
+++ b/numba_dpex/tests/dpjit_tests/test_dpjit_complex_arg_types.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/test_dpjit_reduction.py b/numba_dpex/tests/dpjit_tests/test_dpjit_reduction.py
index ca0e33913c..4119c7152e 100644
--- a/numba_dpex/tests/dpjit_tests/test_dpjit_reduction.py
+++ b/numba_dpex/tests/dpjit_tests/test_dpjit_reduction.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/dpjit_tests/test_slicing.py b/numba_dpex/tests/dpjit_tests/test_slicing.py
index 7d03751178..6ecab1ed7c 100644
--- a/numba_dpex/tests/dpjit_tests/test_slicing.py
+++ b/numba_dpex/tests/dpjit_tests/test_slicing.py
@@ -1,9 +1,9 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
 """
-Tests for boxing for dpnp.ndarray
+Tests for slicing dpnp.ndarray
 """
 
 import dpnp
diff --git a/numba_dpex/tests/experimental/IntEnumLiteral/__init__.py b/numba_dpex/tests/experimental/IntEnumLiteral/__init__.py
new file mode 100644
index 0000000000..ee20629a35
--- /dev/null
+++ b/numba_dpex/tests/experimental/IntEnumLiteral/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/tests/experimental/IntEnumLiteral/test_compilation.py b/numba_dpex/tests/experimental/IntEnumLiteral/test_compilation.py
new file mode 100644
index 0000000000..31a0f6be56
--- /dev/null
+++ b/numba_dpex/tests/experimental/IntEnumLiteral/test_compilation.py
@@ -0,0 +1,36 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpnp
+
+import numba_dpex.experimental as exp_dpex
+from numba_dpex import Range
+from numba_dpex.experimental.flag_enum import FlagEnum
+
+
+class MockFlags(FlagEnum):
+    FLAG1 = 100
+    FLAG2 = 200
+
+
+@exp_dpex.kernel(
+    release_gil=False,
+    no_compile=True,
+    no_cpython_wrapper=True,
+    no_cfunc_wrapper=True,
+)
+def update_with_flag(a):
+    a[0] = MockFlags.FLAG1
+    a[1] = MockFlags.FLAG2
+
+
+def test_compilation_of_flag_enum():
+    """Tests if a FlagEnum subclass can be used inside a kernel function."""
+    a = dpnp.ones(10, dtype=dpnp.int64)
+    exp_dpex.call_kernel(update_with_flag, Range(10), a)
+
+    assert a[0] == MockFlags.FLAG1
+    assert a[1] == MockFlags.FLAG2
+    for idx in range(2, 9):
+        assert a[idx] == 1
diff --git a/numba_dpex/tests/experimental/IntEnumLiteral/test_type_creation.py b/numba_dpex/tests/experimental/IntEnumLiteral/test_type_creation.py
new file mode 100644
index 0000000000..3e974b973d
--- /dev/null
+++ b/numba_dpex/tests/experimental/IntEnumLiteral/test_type_creation.py
@@ -0,0 +1,30 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from enum import IntEnum
+
+import pytest
+
+from numba_dpex.core.exceptions import IllegalIntEnumLiteralValueError
+from numba_dpex.experimental import IntEnumLiteral
+from numba_dpex.experimental.flag_enum import FlagEnum
+
+
+def test_intenumliteral_creation():
+    """Tests the creation of an IntEnumLiteral type."""
+
+    class DummyFlags(FlagEnum):
+        DUMMY = 0
+
+    try:
+        IntEnumLiteral(DummyFlags)
+    except:
+        pytest.fail("Unexpected failure in IntEnumLiteral initialization")
+
+    with pytest.raises(IllegalIntEnumLiteralValueError):
+
+        class SomeKindOfUnknownEnum(IntEnum):
+            UNKNOWN_FLAG = 1
+
+        IntEnumLiteral(SomeKindOfUnknownEnum)
diff --git a/numba_dpex/tests/experimental/IntEnumLiteral/test_type_registration.py b/numba_dpex/tests/experimental/IntEnumLiteral/test_type_registration.py
new file mode 100644
index 0000000000..3bcbef8691
--- /dev/null
+++ b/numba_dpex/tests/experimental/IntEnumLiteral/test_type_registration.py
@@ -0,0 +1,36 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import pytest
+from numba.core.datamodel import default_manager
+
+from numba_dpex.core.datamodel.models import dpex_data_model_manager
+from numba_dpex.experimental import IntEnumLiteral
+from numba_dpex.experimental.flag_enum import FlagEnum
+from numba_dpex.experimental.models import exp_dmm
+
+
+def test_data_model_registration():
+    """Tests that the IntEnumLiteral type is only registered with the
+    DpexExpKernelTargetContext target.
+    """
+
+    class DummyFlags(FlagEnum):
+        DUMMY = 0
+
+    dummy = IntEnumLiteral(DummyFlags)
+
+    with pytest.raises(KeyError):
+        default_manager.lookup(dummy)
+
+    with pytest.raises(KeyError):
+        dpex_data_model_manager.lookup(dummy)
+
+    try:
+        exp_dmm.lookup(dummy)
+    except:
+        pytest.fail(
+            "IntEnumLiteral type lookup failed in experimental "
+            "data model manager"
+        )
diff --git a/numba_dpex/tests/experimental/__init__.py b/numba_dpex/tests/experimental/__init__.py
new file mode 100644
index 0000000000..1d329c9611
--- /dev/null
+++ b/numba_dpex/tests/experimental/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/tests/experimental/codegen/__init__.py b/numba_dpex/tests/experimental/codegen/__init__.py
new file mode 100644
index 0000000000..1d329c9611
--- /dev/null
+++ b/numba_dpex/tests/experimental/codegen/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/tests/experimental/codegen/test_inline_threshold_codegen.py b/numba_dpex/tests/experimental/codegen/test_inline_threshold_codegen.py
new file mode 100644
index 0000000000..bafb430903
--- /dev/null
+++ b/numba_dpex/tests/experimental/codegen/test_inline_threshold_codegen.py
@@ -0,0 +1,73 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpctl
+from numba.core import types
+
+import numba_dpex as dpex
+from numba_dpex import DpctlSyclQueue, DpnpNdArray
+from numba_dpex import experimental as dpex_exp
+from numba_dpex import int64
+
+
+def kernel_func(a, b, c):
+    i = dpex.get_global_id(0)
+    c[i] = a[i] + b[i]
+
+
+def test_codegen_with_max_inline_threshold():
+    """Tests if the inline_threshold option leads to a fully inlined kernel
+    function generation.
+
+    By default, numba_dpex compiles a function passed to the `kernel` decorator
+    into a `spir_func` LLVM function. Then before lowering to device IR, the
+    DpexTargetContext creates a "wrapper" function that has the "spir_kernel"
+    calling convention. It is done so that we can use the same target context
+    and pipeline to compile both host callable "kernels" and device-only
+    "device_func" functions.
+
+    Unless the inline_threshold is set to 3, the `spir_func` function is not
+    inlined into the wrapper function. The test checks if the `spir_func`
+    function is fully inlined into the wrapper. The test is rather rudimentary
+    and only checks the count of function in the generated module.
+    With inlining, the count should be one and without inlining it will be two.
+    """
+
+    queue_ty = DpctlSyclQueue(dpctl.SyclQueue())
+    i64arr_ty = DpnpNdArray(ndim=1, dtype=int64, layout="C", queue=queue_ty)
+    kernel_sig = types.void(i64arr_ty, i64arr_ty, i64arr_ty)
+
+    disp = dpex_exp.kernel(inline_threshold=3)(kernel_func)
+    disp.compile(kernel_sig)
+    kcres = disp.overloads[kernel_sig.args]
+    llvm_ir_mod = kcres.library._final_module
+
+    count_of_non_declaration_type_functions = 0
+
+    for f in llvm_ir_mod.functions:
+        if not f.is_declaration:
+            count_of_non_declaration_type_functions += 1
+
+    assert count_of_non_declaration_type_functions == 1
+
+
+def test_codegen_without_max_inline_threshold():
+    """See docstring of :func:`test_codegen_with_max_inline_threshold`."""
+
+    queue_ty = DpctlSyclQueue(dpctl.SyclQueue())
+    i64arr_ty = DpnpNdArray(ndim=1, dtype=int64, layout="C", queue=queue_ty)
+    kernel_sig = types.void(i64arr_ty, i64arr_ty, i64arr_ty)
+
+    disp = dpex_exp.kernel(kernel_func)
+    disp.compile(kernel_sig)
+    kcres = disp.overloads[kernel_sig.args]
+    llvm_ir_mod = kcres.library._final_module
+
+    count_of_non_declaration_type_functions = 0
+
+    for f in llvm_ir_mod.functions:
+        if not f.is_declaration:
+            count_of_non_declaration_type_functions += 1
+
+    assert count_of_non_declaration_type_functions == 2
diff --git a/numba_dpex/tests/experimental/codegen/test_intenum_literal_codegen.py b/numba_dpex/tests/experimental/codegen/test_intenum_literal_codegen.py
new file mode 100644
index 0000000000..77233e7bd9
--- /dev/null
+++ b/numba_dpex/tests/experimental/codegen/test_intenum_literal_codegen.py
@@ -0,0 +1,53 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import re
+
+import dpctl
+from numba.core import types
+
+import numba_dpex.experimental as exp_dpex
+from numba_dpex import DpctlSyclQueue, DpnpNdArray, int64
+from numba_dpex.experimental.flag_enum import FlagEnum
+
+
+def test_compilation_as_literal_constant():
+    """Tests if FlagEnum objects are treaded as scalar constants inside
+    numba-dpex generated code.
+
+    The test case compiles the kernel `pass_flags_to_func` that includes a
+    call to the device_func `bitwise_or_flags`. The `bitwise_or_flags` function
+    is passed two FlagEnum arguments. The test case evaluates the generated
+    LLVM IR for `pass_flags_to_func` to see if the call to `bitwise_or_flags`
+    has the scalar arguments `i64 1` and `i64 2`.
+    """
+
+    class PseudoFlags(FlagEnum):
+        FLAG1 = 1
+        FLAG2 = 2
+
+    @exp_dpex.device_func
+    def bitwise_or_flags(flag1, flag2):
+        return flag1 | flag2
+
+    def pass_flags_to_func(a):
+        f1 = PseudoFlags.FLAG1
+        f2 = PseudoFlags.FLAG2
+        a[0] = bitwise_or_flags(f1, f2)
+
+    queue_ty = DpctlSyclQueue(dpctl.SyclQueue())
+    i64arr_ty = DpnpNdArray(ndim=1, dtype=int64, layout="C", queue=queue_ty)
+    kernel_sig = types.void(i64arr_ty)
+
+    disp = exp_dpex.kernel(pass_flags_to_func)
+    disp.compile(kernel_sig)
+    kcres = disp.overloads[kernel_sig.args]
+    llvm_ir_mod = kcres.library._final_module.__str__()
+
+    pattern = re.compile(
+        r"call spir_func i32 @\_Z.*bitwise\_or"
+        r"\_flags.*\(i64\* nonnull %.*, i64 1, i64 2\)"
+    )
+
+    assert re.search(pattern, llvm_ir_mod) is not None
diff --git a/numba_dpex/tests/experimental/kernel_iface/__init__.py b/numba_dpex/tests/experimental/kernel_iface/__init__.py
new file mode 100644
index 0000000000..1d329c9611
--- /dev/null
+++ b/numba_dpex/tests/experimental/kernel_iface/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/tests/experimental/kernel_iface/spv_overloads/__init__.py b/numba_dpex/tests/experimental/kernel_iface/spv_overloads/__init__.py
new file mode 100644
index 0000000000..1d329c9611
--- /dev/null
+++ b/numba_dpex/tests/experimental/kernel_iface/spv_overloads/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
diff --git a/numba_dpex/tests/experimental/kernel_iface/spv_overloads/test_atomic_fetch_phi.py b/numba_dpex/tests/experimental/kernel_iface/spv_overloads/test_atomic_fetch_phi.py
new file mode 100644
index 0000000000..4dd02066a5
--- /dev/null
+++ b/numba_dpex/tests/experimental/kernel_iface/spv_overloads/test_atomic_fetch_phi.py
@@ -0,0 +1,154 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpnp
+import pytest
+from numba.core.errors import TypingError
+
+import numba_dpex as dpex
+import numba_dpex.experimental as dpex_exp
+from numba_dpex.experimental.kernel_iface import AtomicRef
+from numba_dpex.tests._helper import get_all_dtypes
+
+list_of_supported_dtypes = get_all_dtypes(
+    no_bool=True, no_float16=True, no_none=True, no_complex=True
+)
+
+list_of_fetch_phi_funcs = [
+    "fetch_add",
+    "fetch_sub",
+    "fetch_min",
+    "fetch_max",
+    "fetch_and",
+    "fetch_or",
+    "fetch_xor",
+]
+
+
+@pytest.fixture(params=list_of_fetch_phi_funcs)
+def fetch_phi_fn(request):
+    return request.param
+
+
+@pytest.fixture(params=list_of_supported_dtypes)
+def input_arrays(request):
+    # The size of input and out arrays to be used
+    N = 10
+    a = dpnp.arange(N, dtype=request.param)
+    b = dpnp.ones(N, dtype=request.param)
+    return a, b
+
+
+@pytest.mark.parametrize("ref_index", [0, 5])
+def test_fetch_phi_fn(input_arrays, ref_index, fetch_phi_fn):
+    """A test for all fetch_phi atomic functions."""
+
+    @dpex_exp.kernel
+    def _kernel(a, b, ref_index):
+        i = dpex.get_global_id(0)
+        v = AtomicRef(b, index=ref_index)
+        getattr(v, fetch_phi_fn)(a[i])
+
+    a, b = input_arrays
+
+    if (
+        fetch_phi_fn in ["fetch_and", "fetch_or", "fetch_xor"]
+        and issubclass(a.dtype.type, dpnp.floating)
+        and issubclass(b.dtype.type, dpnp.floating)
+    ):
+        # fetch_and, fetch_or, fetch_xor accept only int arguments.
+        # test for TypingError when float arguments are passed.
+        with pytest.raises(TypingError):
+            dpex_exp.call_kernel(_kernel, dpex.Range(10), a, b, ref_index)
+    else:
+        dpex_exp.call_kernel(_kernel, dpex.Range(10), a, b, ref_index)
+        # Verify that `a` accumulated at b[ref_index] by kernel
+        # matches the `a` accumulated at  b[ref_index+1] using Python
+        for i in range(a.size):
+            v = AtomicRef(b, index=ref_index + 1)
+            getattr(v, fetch_phi_fn)(a[i])
+
+        assert b[ref_index] == b[ref_index + 1]
+
+
+def test_fetch_phi_diff_types(fetch_phi_fn):
+    """A negative test that verifies that a TypingError is raised if
+    AtomicRef type and value to be added are of different types.
+    """
+
+    @dpex_exp.kernel
+    def _kernel(a, b):
+        i = dpex.get_global_id(0)
+        v = AtomicRef(b, index=0)
+        getattr(v, fetch_phi_fn)(a[i])
+
+    N = 10
+    a = dpnp.ones(N, dtype=dpnp.float32)
+    b = dpnp.zeros(N, dtype=dpnp.int32)
+
+    with pytest.raises(TypingError):
+        dpex_exp.call_kernel(_kernel, dpex.Range(10), a, b)
+
+
+@dpex_exp.kernel
+def atomic_ref_0(a):
+    i = dpex.get_global_id(0)
+    v = AtomicRef(a, index=0)
+    v.fetch_add(a[i + 2])
+
+
+@dpex_exp.kernel
+def atomic_ref_1(a):
+    i = dpex.get_global_id(0)
+    v = AtomicRef(a, index=1)
+    v.fetch_add(a[i + 2])
+
+
+def test_spirv_compiler_flags_add():
+    """Check if float atomic flag is being populated from intrinsic for the
+    second call.
+
+    https://github.com/IntelPython/numba-dpex/issues/1262
+    """
+    N = 10
+    a = dpnp.ones(N, dtype=dpnp.float32)
+
+    dpex_exp.call_kernel(atomic_ref_0, dpex.Range(N - 2), a)
+    dpex_exp.call_kernel(atomic_ref_1, dpex.Range(N - 2), a)
+
+    assert a[0] == N - 1
+    assert a[1] == N - 1
+
+
+@dpex_exp.kernel
+def atomic_max_0(a):
+    i = dpex.get_global_id(0)
+    v = AtomicRef(a, index=0)
+    if i != 0:
+        v.fetch_max(a[i])
+
+
+@dpex_exp.kernel
+def atomic_max_1(a):
+    i = dpex.get_global_id(0)
+    v = AtomicRef(a, index=0)
+    if i != 0:
+        v.fetch_max(a[i])
+
+
+def test_spirv_compiler_flags_max():
+    """Check if float atomic flag is being populated from intrinsic for the
+    second call.
+
+    https://github.com/IntelPython/numba-dpex/issues/1262
+    """
+    N = 10
+    a = dpnp.arange(N, dtype=dpnp.float32)
+    b = dpnp.arange(N, dtype=dpnp.float32)
+
+    dpex_exp.call_kernel(atomic_max_0, dpex.Range(N), a)
+    dpex_exp.call_kernel(atomic_max_1, dpex.Range(N), b)
+
+    assert a[0] == N - 1
+    assert b[0] == N - 1
diff --git a/numba_dpex/tests/experimental/kernel_iface/spv_overloads/test_atomic_ref.py b/numba_dpex/tests/experimental/kernel_iface/spv_overloads/test_atomic_ref.py
new file mode 100644
index 0000000000..60f31de0ea
--- /dev/null
+++ b/numba_dpex/tests/experimental/kernel_iface/spv_overloads/test_atomic_ref.py
@@ -0,0 +1,45 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpnp
+import pytest
+from numba.core.errors import TypingError
+
+import numba_dpex as dpex
+import numba_dpex.experimental as dpex_exp
+from numba_dpex.experimental.kernel_iface import AddressSpace, AtomicRef
+
+
+def test_atomic_ref_compilation():
+    @dpex_exp.kernel
+    def atomic_ref_kernel(a, b):
+        i = dpex.get_global_id(0)
+        v = AtomicRef(b, index=0, address_space=AddressSpace.GLOBAL)
+        v.fetch_add(a[i])
+
+    a = dpnp.ones(10)
+    b = dpnp.zeros(10)
+    try:
+        dpex_exp.call_kernel(atomic_ref_kernel, dpex.Range(10), a, b)
+    except Exception:
+        pytest.fail("Unexpected execution failure")
+
+
+def test_atomic_ref_compilation_failure():
+    """A negative test that verifies that a TypingError is raised if we try to
+    create an AtomicRef in the local address space from a global address space
+    ref.
+    """
+
+    @dpex_exp.kernel
+    def atomic_ref_kernel(a, b):
+        i = dpex.get_global_id(0)
+        v = AtomicRef(b, index=0, address_space=AddressSpace.LOCAL)
+        v.fetch_add(a[i])
+
+    a = dpnp.ones(10)
+    b = dpnp.zeros(10)
+
+    with pytest.raises(TypingError):
+        dpex_exp.call_kernel(atomic_ref_kernel, dpex.Range(10), a, b)
diff --git a/numba_dpex/tests/experimental/kernel_iface/test_memory_enum_compilation.py b/numba_dpex/tests/experimental/kernel_iface/test_memory_enum_compilation.py
new file mode 100644
index 0000000000..a9dae36678
--- /dev/null
+++ b/numba_dpex/tests/experimental/kernel_iface/test_memory_enum_compilation.py
@@ -0,0 +1,78 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpnp
+
+import numba_dpex.experimental as exp_dpex
+from numba_dpex import Range
+from numba_dpex.experimental.kernel_iface import (
+    AddressSpace,
+    MemoryOrder,
+    MemoryScope,
+)
+
+
+def test_compilation_of_memory_order():
+    """Tests if a MemoryOrder flags can be used inside a kernel function."""
+
+    @exp_dpex.kernel
+    def store_memory_order_flag(a):
+        a[0] = MemoryOrder.RELAXED
+        a[1] = MemoryOrder.CONSUME_UNSUPPORTED
+        a[2] = MemoryOrder.ACQ_REL
+        a[3] = MemoryOrder.ACQUIRE
+        a[4] = MemoryOrder.RELEASE
+        a[5] = MemoryOrder.SEQ_CST
+
+    a = dpnp.ones(10, dtype=dpnp.int64)
+    exp_dpex.call_kernel(store_memory_order_flag, Range(10), a)
+
+    assert a[0] == MemoryOrder.RELAXED
+    assert a[1] == MemoryOrder.CONSUME_UNSUPPORTED
+    assert a[2] == MemoryOrder.ACQ_REL
+    assert a[3] == MemoryOrder.ACQUIRE
+    assert a[4] == MemoryOrder.RELEASE
+    assert a[5] == MemoryOrder.SEQ_CST
+
+
+def test_compilation_of_memory_scope():
+    """Tests if a MemoryScope flags can be used inside a kernel function."""
+
+    @exp_dpex.kernel
+    def store_memory_scope_flag(a):
+        a[0] = MemoryScope.DEVICE
+        a[1] = MemoryScope.SUB_GROUP
+        a[2] = MemoryScope.WORK_GROUP
+        a[3] = MemoryScope.SYSTEM
+        a[4] = MemoryScope.WORK_ITEM
+
+    a = dpnp.ones(10, dtype=dpnp.int64)
+    exp_dpex.call_kernel(store_memory_scope_flag, Range(10), a)
+
+    assert a[0] == MemoryScope.DEVICE
+    assert a[1] == MemoryScope.SUB_GROUP
+    assert a[2] == MemoryScope.WORK_GROUP
+    assert a[3] == MemoryScope.SYSTEM
+    assert a[4] == MemoryScope.WORK_ITEM
+
+
+def test_compilation_of_address_space():
+    """Tests if a AddressSpace flags can be used inside a kernel function."""
+
+    @exp_dpex.kernel
+    def store_address_space_flag(a):
+        a[0] = AddressSpace.CONSTANT
+        a[1] = AddressSpace.GENERIC
+        a[2] = AddressSpace.GLOBAL
+        a[3] = AddressSpace.LOCAL
+        a[4] = AddressSpace.PRIVATE
+
+    a = dpnp.ones(10, dtype=dpnp.int64)
+    exp_dpex.call_kernel(store_address_space_flag, Range(10), a)
+
+    assert a[0] == AddressSpace.CONSTANT
+    assert a[1] == AddressSpace.GENERIC
+    assert a[2] == AddressSpace.GLOBAL
+    assert a[3] == AddressSpace.LOCAL
+    assert a[4] == AddressSpace.PRIVATE
diff --git a/numba_dpex/tests/experimental/test_async_kernel.py b/numba_dpex/tests/experimental/test_async_kernel.py
new file mode 100644
index 0000000000..8e772e2891
--- /dev/null
+++ b/numba_dpex/tests/experimental/test_async_kernel.py
@@ -0,0 +1,114 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpctl
+import dpnp
+import pytest
+from numba.core.errors import TypingError
+
+import numba_dpex as dpex
+import numba_dpex.experimental as exp_dpex
+from numba_dpex import Range
+from numba_dpex.experimental import testing
+
+
+@exp_dpex.kernel(
+    release_gil=False,
+    no_compile=True,
+    no_cpython_wrapper=True,
+    no_cfunc_wrapper=True,
+)
+def add(a, b, c):
+    i = dpex.get_global_id(0)
+    c[i] = b[i] + a[i]
+
+
+def test_async_add():
+    size = 10
+    a = dpnp.ones(size)
+    b = dpnp.ones(size)
+    c = dpnp.zeros(size)
+
+    r = Range(size)
+
+    host_ref, event_ref = exp_dpex.call_kernel_async(
+        add,
+        r,
+        (),
+        a,
+        b,
+        c,
+    )
+
+    assert type(host_ref) == dpctl.SyclEvent
+    assert type(event_ref) == dpctl.SyclEvent
+    assert host_ref is not None
+    assert event_ref is not None
+
+    event_ref.wait()
+    host_ref.wait()
+
+    d = a + b
+    assert dpnp.array_equal(c, d)
+
+
+def test_async_dependent_add_list_exception():
+    """Checks either ValueError is triggered if list was passed instead of
+    tuple for the dependent events."""
+    size = 10
+
+    # TODO: should capture ValueError, but numba captures it and generates
+    # TypingError. ValueError is still readable there.
+    with pytest.raises(TypingError):
+        exp_dpex.call_kernel_async(
+            add,
+            Range(size),
+            [dpctl.SyclEvent()],
+            dpnp.ones(size),
+            dpnp.ones(size),
+            dpnp.ones(size),
+        )
+
+
+def test_async_dependent_add():
+    size = 10
+    a = dpnp.ones(size)
+    b = dpnp.ones(size)
+    c = dpnp.zeros(size)
+
+    r = Range(size)
+
+    host_ref, event_ref = exp_dpex.call_kernel_async(
+        add,
+        r,
+        (),
+        a,
+        b,
+        c,
+    )
+
+    host2_ref, event2_ref = exp_dpex.call_kernel_async(
+        add,
+        r,
+        (event_ref,),
+        a,
+        c,
+        b,
+    )
+
+    event2_ref.wait()
+    d = dpnp.ones(size) * 3
+    assert dpnp.array_equal(b, d)
+
+    host_ref.wait()
+    host2_ref.wait()
+
+
+def test_async_add_from_cache():
+    test_async_add()  # compile
+    old_size = testing.kernel_cache_size()
+    test_async_add()  # use from cache
+    new_size = testing.kernel_cache_size()
+
+    assert new_size == old_size
diff --git a/numba_dpex/tests/experimental/test_compiler_warnings.py b/numba_dpex/tests/experimental/test_compiler_warnings.py
new file mode 100644
index 0000000000..5cbc2bc9e6
--- /dev/null
+++ b/numba_dpex/tests/experimental/test_compiler_warnings.py
@@ -0,0 +1,34 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpctl
+import pytest
+from numba.core import types
+
+import numba_dpex as dpex
+from numba_dpex import DpctlSyclQueue, DpnpNdArray
+from numba_dpex import experimental as dpex_exp
+from numba_dpex import int64
+
+
+def _kernel(a, b, c):
+    i = dpex.get_global_id(0)
+    c[i] = a[i] + b[i]
+
+
+def test_compilation_mode_option_user_definition():
+    with pytest.warns(UserWarning):
+        dpex_exp.kernel(_compilation_mode="kernel")(_kernel)
+
+
+def test_inline_threshold_level_warning():
+    """
+    Test compiler warning generation with an inline_threshold value of 3.
+    """
+
+    with pytest.warns(UserWarning):
+        queue_ty = DpctlSyclQueue(dpctl.SyclQueue())
+        i64arr_ty = DpnpNdArray(ndim=1, dtype=int64, layout="C", queue=queue_ty)
+        kernel_sig = types.void(i64arr_ty, i64arr_ty, i64arr_ty)
+        dpex_exp.kernel(inline_threshold=3)(_kernel).compile(kernel_sig)
diff --git a/numba_dpex/tests/experimental/test_exec_queue_inference.py b/numba_dpex/tests/experimental/test_exec_queue_inference.py
new file mode 100644
index 0000000000..b5df39c427
--- /dev/null
+++ b/numba_dpex/tests/experimental/test_exec_queue_inference.py
@@ -0,0 +1,91 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpctl
+import dpnp
+import pytest
+from numba.core import config
+
+import numba_dpex.experimental as exp_dpex
+from numba_dpex import Range
+from numba_dpex.core.exceptions import ExecutionQueueInferenceError
+
+
+@exp_dpex.kernel(
+    release_gil=False,
+    no_compile=True,
+    no_cpython_wrapper=True,
+    no_cfunc_wrapper=True,
+)
+def add(a, b, c):
+    c[0] = b[0] + a[0]
+
+
+def test_successful_execution_queue_inference():
+    """
+    Tests if KernelDispatcher successfully infers the execution queue for the
+    kernel.
+    """
+
+    q = dpctl.SyclQueue()
+    a = dpnp.ones(100, sycl_queue=q)
+    b = dpnp.ones_like(a, sycl_queue=q)
+    c = dpnp.zeros_like(a, sycl_queue=q)
+    r = Range(100)
+
+    current_captured_error_style = config.CAPTURED_ERRORS
+    config.CAPTURED_ERRORS = "new_style"
+
+    try:
+        exp_dpex.call_kernel(add, r, a, b, c)
+    except:
+        pytest.fail("Unexpected error when calling kernel")
+
+    config.CAPTURED_ERRORS = current_captured_error_style
+
+    assert c[0] == b[0] + a[0]
+
+
+def test_execution_queue_inference_error():
+    """
+    Tests if KernelDispatcher successfully raised ExecutionQueueInferenceError
+    when dpnp.ndarray arguments do not share the same dpctl.SyclQueue
+    instance.
+    """
+
+    q1 = dpctl.SyclQueue()
+    q2 = dpctl.SyclQueue()
+    a = dpnp.ones(100, sycl_queue=q1)
+    b = dpnp.ones_like(a, sycl_queue=q2)
+    c = dpnp.zeros_like(a, sycl_queue=q1)
+    r = Range(100)
+
+    current_captured_error_style = config.CAPTURED_ERRORS
+    config.CAPTURED_ERRORS = "new_style"
+
+    with pytest.raises(ExecutionQueueInferenceError):
+        exp_dpex.call_kernel(add, r, a, b, c)
+
+    config.CAPTURED_ERRORS = current_captured_error_style
+
+
+def test_error_when_no_array_args():
+    """
+    Tests if KernelDispatcher successfully raised ExecutionQueueInferenceError
+    when no dpnp.ndarray arguments were passed to a kernel.
+    """
+    a = 1
+    b = 2
+    c = 3
+    r = Range(100)
+
+    from numba.core import config
+
+    current_captured_error_style = config.CAPTURED_ERRORS
+    config.CAPTURED_ERRORS = "new_style"
+
+    with pytest.raises(ExecutionQueueInferenceError):
+        exp_dpex.call_kernel(add, r, a, b, c)
+
+    config.CAPTURED_ERRORS = current_captured_error_style
diff --git a/numba_dpex/tests/experimental/test_inline_threshold_config.py b/numba_dpex/tests/experimental/test_inline_threshold_config.py
new file mode 100644
index 0000000000..c90843f20b
--- /dev/null
+++ b/numba_dpex/tests/experimental/test_inline_threshold_config.py
@@ -0,0 +1,59 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from numba.core import compiler
+
+import numba_dpex as dpex
+from numba_dpex import experimental as dpex_exp
+
+
+def kernel_func(a, b, c):
+    i = dpex.get_global_id(0)
+    c[i] = a[i] + b[i]
+
+
+def test_inline_threshold_set_using_config():
+    oldConfig = dpex.config.INLINE_THRESHOLD
+    dpex.config.INLINE_THRESHOLD = None
+
+    disp = dpex_exp.kernel(kernel_func)
+    flags = compiler.Flags()
+    disp.targetdescr.options.parse_as_flags(flags, disp.targetoptions)
+
+    assert flags.inline_threshold == 0
+
+    dpex.config.INLINE_THRESHOLD = 2
+
+    flags = compiler.Flags()
+    disp.targetdescr.options.parse_as_flags(flags, disp.targetoptions)
+
+    assert flags.inline_threshold == 2
+
+    dpex.config.INLINE_THRESHOLD = oldConfig
+
+
+def test_inline_threshold_set_using_decorator_option():
+    """
+    Test setting the inline_threshold value using the kernel decorator flag
+    """
+
+    disp = dpex_exp.kernel(inline_threshold=2)(kernel_func)
+    flags = compiler.Flags()
+    disp.targetdescr.options.parse_as_flags(flags, disp.targetoptions)
+
+    assert flags.inline_threshold == 2
+
+
+def test_inline_threshold_set_using_decorator_supersedes_config_option():
+    oldConfig = dpex.config.INLINE_THRESHOLD
+    dpex.config.INLINE_THRESHOLD = None
+
+    disp = dpex_exp.kernel(inline_threshold=3)(kernel_func)
+    flags = compiler.Flags()
+    disp.targetdescr.options.parse_as_flags(flags, disp.targetoptions)
+
+    print(flags.inline_threshold)
+    assert flags.inline_threshold == 3
+
+    dpex.config.INLINE_THRESHOLD = oldConfig
diff --git a/numba_dpex/tests/experimental/test_kernel_dispatcher.py b/numba_dpex/tests/experimental/test_kernel_dispatcher.py
new file mode 100644
index 0000000000..9c41bf9960
--- /dev/null
+++ b/numba_dpex/tests/experimental/test_kernel_dispatcher.py
@@ -0,0 +1,105 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpctl
+import dpnp
+
+import numba_dpex.experimental as exp_dpex
+from numba_dpex import NdRange, Range, dpjit
+
+
+@exp_dpex.kernel(
+    release_gil=False,
+    no_compile=True,
+    no_cpython_wrapper=True,
+    no_cfunc_wrapper=True,
+)
+def add(a, b, c):
+    c[0] = b[0] + a[0]
+
+
+@exp_dpex.kernel(
+    release_gil=False,
+    no_compile=True,
+    no_cpython_wrapper=True,
+    no_cfunc_wrapper=True,
+)
+def sq(a, b):
+    a[0] = b[0] * b[0]
+
+
+def test_call_kernel_from_cpython():
+    """
+    Tests if we can call a kernel function from CPython using the call_kernel
+    dpjit function.
+    """
+
+    q = dpctl.SyclQueue()
+    a = dpnp.ones(100, sycl_queue=q)
+    b = dpnp.ones_like(a, sycl_queue=q)
+    c = dpnp.zeros_like(a, sycl_queue=q)
+    r = Range(100)
+    ndr = NdRange(global_size=(100,), local_size=(1,))
+
+    exp_dpex.call_kernel(add, r, a, b, c)
+
+    assert c[0] == b[0] + a[0]
+
+    exp_dpex.call_kernel(add, ndr, a, b, c)
+
+    assert c[0] == b[0] + a[0]
+
+
+def test_call_kernel_from_dpjit():
+    """
+    Tests if we can call a kernel function from a dpjit function using the
+    call_kernel dpjit function.
+    """
+
+    @dpjit
+    def range_kernel_caller(q, a, b, c):
+        r = Range(100)
+        exp_dpex.call_kernel(add, r, a, b, c)
+        return c
+
+    @dpjit
+    def ndrange_kernel_caller(q, a, b, c):
+        gr = Range(100)
+        lr = Range(1)
+        ndr = NdRange(gr, lr)
+        exp_dpex.call_kernel(add, ndr, a, b, c)
+        return c
+
+    q = dpctl.SyclQueue()
+    a = dpnp.ones(100, sycl_queue=q)
+    b = dpnp.ones_like(a, sycl_queue=q)
+    c = dpnp.zeros_like(a, sycl_queue=q)
+
+    range_kernel_caller(q, a, b, c)
+
+    assert c[0] == b[0] + a[0]
+
+    ndrange_kernel_caller(q, a, b, c)
+
+    assert c[0] == b[0] + a[0]
+
+
+def test_call_multiple_kernels():
+    """
+    Tests if the call_kernel dpjit function supports calling different types of
+    kernel with different number of arguments.
+    """
+    q = dpctl.SyclQueue()
+    a = dpnp.ones(100, sycl_queue=q)
+    b = dpnp.ones_like(a, sycl_queue=q)
+    c = dpnp.zeros_like(a, sycl_queue=q)
+    r = Range(100)
+
+    exp_dpex.call_kernel(add, r, a, b, c)
+
+    assert c[0] == b[0] + a[0]
+
+    exp_dpex.call_kernel(sq, r, a, c)
+
+    assert a[0] == c[0] * c[0]
diff --git a/numba_dpex/tests/experimental/test_kernel_specialization.py b/numba_dpex/tests/experimental/test_kernel_specialization.py
new file mode 100644
index 0000000000..2d1fbbaa02
--- /dev/null
+++ b/numba_dpex/tests/experimental/test_kernel_specialization.py
@@ -0,0 +1,101 @@
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpctl.tensor as dpt
+import dpnp
+import pytest
+from numba.core.errors import TypingError
+
+import numba_dpex as dpex
+import numba_dpex.experimental as dpex_exp
+from numba_dpex import DpnpNdArray, float32, int64
+from numba_dpex.core.exceptions import InvalidKernelSpecializationError
+from numba_dpex.core.kernel_interface.indexers import Range
+
+i64arrty = DpnpNdArray(ndim=1, dtype=int64, layout="C")
+f32arrty = DpnpNdArray(ndim=1, dtype=float32, layout="C")
+
+specialized_kernel1 = dpex_exp.kernel((i64arrty, i64arrty, i64arrty))
+specialized_kernel2 = dpex_exp.kernel(
+    [(i64arrty, i64arrty, i64arrty), (f32arrty, f32arrty, f32arrty)]
+)
+
+
+def data_parallel_sum(a, b, c):
+    """
+    Vector addition using the ``kernel`` decorator.
+    """
+    i = dpex.get_global_id(0)
+    c[i] = a[i] + b[i]
+
+
+def test_single_specialization():
+    """Test if a kernel can be specialized with a single signature."""
+    jitkernel = specialized_kernel1(data_parallel_sum)
+    assert len(jitkernel.overloads) == 1
+
+
+def test_multiple_specialization():
+    """Test if a kernel can be specialized with multiple signatures."""
+    jitkernel = specialized_kernel2(data_parallel_sum)
+    assert len(jitkernel.overloads) == 2
+
+
+def test_invalid_specialization_error():
+    """Test if an InvalidKernelSpecializationError is raised when attempting to
+    specialize with NumPy arrays.
+    """
+    specialized_kernel3 = dpex_exp.kernel((int64[::1], int64[::1], int64[::1]))
+    with pytest.raises(InvalidKernelSpecializationError):
+        specialized_kernel3(data_parallel_sum)
+
+
+def test_missing_specialization_error():
+    """Test if a MissingSpecializationError is raised when calling a
+    specialized kernel with unsupported arguments.
+    """
+    SIZE = 16
+    a = dpnp.ones(SIZE, dtype=dpt.int32)
+    b = dpnp.ones(SIZE, dtype=dpt.int32)
+    c = dpnp.zeros(SIZE, dtype=dpt.int32)
+
+    with pytest.raises(TypingError):
+        data_parallel_sum_specialized = specialized_kernel1(data_parallel_sum)
+        dpex_exp.call_kernel(
+            data_parallel_sum_specialized, Range(SIZE), a, b, c
+        )
+
+
+def test_execution_of_specialized_kernel():
+    """Test if the specialized kernel is correctly executed."""
+    SIZE = 16
+
+    a = dpnp.ones(SIZE, dtype=dpt.int64)
+    b = dpnp.ones(SIZE, dtype=dpt.int64)
+    c = dpnp.zeros(SIZE, dtype=dpt.int64)
+
+    data_parallel_sum_specialized = specialized_kernel1(data_parallel_sum)
+
+    dpex_exp.call_kernel(data_parallel_sum_specialized, Range(SIZE), a, b, c)
+
+    npc = dpnp.asnumpy(c)
+    import numpy as np
+
+    npc_expected = np.full(SIZE, 2, dtype=np.int64)
+    assert np.array_equal(npc, npc_expected)
+
+
+def test_string_specialization():
+    """Test if NotImplementedError is raised when signature is a string"""
+
+    with pytest.raises(NotImplementedError):
+        dpex_exp.kernel("(i64arrty, i64arrty, i64arrty)")
+
+    with pytest.raises(NotImplementedError):
+        dpex_exp.kernel(
+            ["(i64arrty, i64arrty, i64arrty)", "(f32arrty, f32arrty, f32arrty)"]
+        )
+
+    with pytest.raises(ValueError):
+        dpex_exp.kernel((i64arrty))
diff --git a/numba_dpex/tests/experimental/test_strided_dpnp_array_in_kernel.py b/numba_dpex/tests/experimental/test_strided_dpnp_array_in_kernel.py
new file mode 100644
index 0000000000..8be4b8e45f
--- /dev/null
+++ b/numba_dpex/tests/experimental/test_strided_dpnp_array_in_kernel.py
@@ -0,0 +1,282 @@
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import math
+
+import dpnp
+import numpy as np
+import pytest
+
+import numba_dpex
+import numba_dpex.experimental as exp_dpex
+from numba_dpex import Range
+
+
+def get_order(a):
+    """Get order of an array.
+
+    Args:
+        a (numpy.ndarray, dpnp.ndarray): Input array.
+
+    Raises:
+        Exception: _description_
+
+    Returns:
+        str: 'C' if c-contiguous, 'F' if f-contiguous or 'A' if aligned.
+    """
+    if a.flags.c_contiguous and not a.flags.f_contiguous:
+        return "C"
+    elif not a.flags.c_contiguous and a.flags.f_contiguous:
+        return "F"
+    elif a.flags.c_contiguous and a.flags.f_contiguous:
+        return "A"
+    else:
+        raise Exception("Unknown order/layout")
+
+
+@exp_dpex.kernel
+def change_values_1d(x, v):
+    """Assign values in a 1d dpnp.ndarray
+
+    Args:
+        x (dpnp.ndarray): Input array.
+        v (int): Value to be assigned.
+    """
+    i = numba_dpex.get_global_id(0)
+    x[i] = v
+
+
+def change_values_1d_func(a, p):
+    """Assign values in a 1d numpy.ndarray
+
+    Args:
+        a (numpy.ndarray): Input array.
+        p (int): Value to be assigned.
+    """
+    for i in range(a.shape[0]):
+        a[i] = p
+
+
+@exp_dpex.kernel
+def change_values_2d(x, v):
+    """Assign values in a 2d dpnp.ndarray
+
+    Args:
+        x (dpnp.ndarray): Input array.
+        v (int): Value to be assigned.
+    """
+    i = numba_dpex.get_global_id(0)
+    j = numba_dpex.get_global_id(1)
+    x[i, j] = v
+
+
+def change_values_2d_func(a, p):
+    """Assign values in a 2d numpy.ndarray
+
+    Args:
+        a (numpy.ndarray): Input array.
+        p (int): Value to be assigned.
+    """
+    for i in range(a.shape[0]):
+        for j in range(a.shape[1]):
+            a[i, j] = p
+
+
+@exp_dpex.kernel
+def change_values_3d(x, v):
+    """Assign values in a 3d dpnp.ndarray
+
+    Args:
+        x (dpnp.ndarray): Input array.
+        v (int): Value to be assigned.
+    """
+    i = numba_dpex.get_global_id(0)
+    j = numba_dpex.get_global_id(1)
+    k = numba_dpex.get_global_id(2)
+    x[i, j, k] = v
+
+
+def change_values_3d_func(a, p):
+    """Assign values in a 3d numpy.ndarray
+
+    Args:
+        a (numpy.ndarray): Input array.
+        p (int): Value to be assigned.
+    """
+    for i in range(a.shape[0]):
+        for j in range(a.shape[1]):
+            for k in range(a.shape[2]):
+                a[i, j, k] = p
+
+
+@pytest.mark.parametrize("s", [1, 2, 3, 4, 5, 6, 7])
+def test_1d_strided_dpnp_array_in_kernel(s):
+    """
+    Tests if we can correctly handle a strided 1d dpnp array
+    inside dpex kernel.
+    """
+    N = 256
+    k = -3
+
+    t = np.arange(0, N, dtype=dpnp.int64)
+    u = dpnp.asarray(t)
+
+    v = u[::s]
+    exp_dpex.call_kernel(change_values_1d, Range(v.shape[0]), v, k)
+
+    x = t[::s]
+    change_values_1d_func(x, k)
+
+    # check the value of the array view
+    assert np.all(dpnp.asnumpy(v) == x)
+    # check the value of the original arrays
+    assert np.all(dpnp.asnumpy(u) == t)
+
+
+@pytest.mark.parametrize("s", [2, 3, 4, 5])
+def test_multievel_1d_strided_dpnp_array_in_kernel(s):
+    """
+    Tests if we can correctly handle a multilevel strided 1d dpnp array
+    inside dpex kernel.
+    """
+    N = 256
+    k = -3
+
+    t = dpnp.arange(0, N, dtype=dpnp.int64)
+    u = dpnp.asarray(t)
+
+    v, x = u, t
+    while v.shape[0] > 1:
+        v = v[::s]
+        exp_dpex.call_kernel(change_values_1d, Range(v.shape[0]), v, k)
+
+        x = x[::s]
+        change_values_1d_func(x, k)
+
+        # check the value of the array view
+        assert np.all(dpnp.asnumpy(v) == x)
+        # check the value of the original arrays
+        assert np.all(dpnp.asnumpy(u) == t)
+
+
+@pytest.mark.parametrize("s1", [2, 4, 6, 8])
+@pytest.mark.parametrize("s2", [1, 3, 5, 7])
+@pytest.mark.parametrize("order", ["C", "F"])
+def test_2d_strided_dpnp_array_in_kernel(s1, s2, order):
+    """
+    Tests if we can correctly handle a strided 2d dpnp array
+    inside dpex kernel.
+    """
+    M, N = 13, 31
+    k = -3
+
+    t = np.arange(0, M * N, dtype=np.int64).reshape(M, N, order=order)
+    u = dpnp.asarray(t)
+
+    # check order, sanity check
+    assert get_order(u) == order
+
+    v = u[::s1, ::s2]
+    exp_dpex.call_kernel(change_values_2d, Range(*v.shape), v, k)
+
+    x = t[::s1, ::s2]
+    change_values_2d_func(x, k)
+
+    # check the value of the array view
+    assert np.all(dpnp.asnumpy(v) == x)
+    # check the value of the original arrays
+    assert np.all(dpnp.asnumpy(u) == t)
+
+
+@pytest.mark.parametrize("s1", [2, 4, 6, 8])
+@pytest.mark.parametrize("s2", [3, 5, 7, 9])
+@pytest.mark.parametrize("order", ["C", "F"])
+def test_multilevel_2d_strided_dpnp_array_in_kernel(s1, s2, order):
+    """
+    Tests if we can correctly handle a multilevel strided 2d dpnp array
+    inside dpex kernel.
+    """
+    M, N = 13, 31
+    k = -3
+
+    t = np.arange(0, M * N, dtype=np.int64).reshape(M, N, order=order)
+    u = dpnp.asarray(t)
+
+    # check order, sanity check
+    assert get_order(u) == order
+
+    v, x = u, t
+    while v.shape[0] > 1 and v.shape[1] > 1:
+        v = v[::s1, ::s2]
+        exp_dpex.call_kernel(change_values_2d, Range(*v.shape), v, k)
+
+        x = x[::s1, ::s2]
+        change_values_2d_func(x, k)
+
+        # check the value of the array view
+        assert np.all(dpnp.asnumpy(v) == x)
+        # check the value of the original arrays
+        assert np.all(dpnp.asnumpy(u) == t)
+
+
+@pytest.mark.parametrize("s1", [1, 2, 3])
+@pytest.mark.parametrize("s2", [2, 3, 4])
+@pytest.mark.parametrize("s3", [3, 4, 5])
+@pytest.mark.parametrize("order", ["C", "F"])
+def test_3d_strided_dpnp_array_in_kernel(s1, s2, s3, order):
+    """
+    Tests if we can correctly handle a strided 3d dpnp array
+    inside dpex kernel.
+    """
+    M, N, K = 13, 31, 11
+    k = -3
+
+    t = np.arange(0, M * N * K, dtype=np.int64).reshape((M, N, K), order=order)
+    u = dpnp.asarray(t)
+
+    # check order, sanity check
+    assert get_order(u) == order
+
+    v = u[::s1, ::s2, ::s3]
+    exp_dpex.call_kernel(change_values_3d, Range(*v.shape), v, k)
+
+    x = t[::s1, ::s2, ::s3]
+    change_values_3d_func(x, k)
+
+    # check the value of the array view
+    assert np.all(dpnp.asnumpy(v) == x)
+    # check the value of the original arrays
+    assert np.all(dpnp.asnumpy(u) == t)
+
+
+@pytest.mark.parametrize("s1", [2, 3, 4])
+@pytest.mark.parametrize("s2", [3, 4, 5])
+@pytest.mark.parametrize("s3", [4, 5, 6])
+@pytest.mark.parametrize("order", ["C", "F"])
+def test_multilevel_3d_strided_dpnp_array_in_kernel(s1, s2, s3, order):
+    """
+    Tests if we can correctly handle a multilevel strided 3d dpnp array
+    inside dpex kernel.
+    """
+    M, N, K = 13, 31, 11
+    k = -3
+
+    t = np.arange(0, M * N * K, dtype=np.int64).reshape((M, N, K), order=order)
+    u = dpnp.asarray(t)
+
+    # check order, sanity check
+    assert get_order(u) == order
+
+    v, x = u, t
+    while v.shape[0] > 1 and v.shape[1] > 1 and v.shape[2] > 1:
+        v = v[::s1, ::s2, ::s3]
+        exp_dpex.call_kernel(change_values_3d, Range(*v.shape), v, k)
+
+        x = x[::s1, ::s2, ::s3]
+        change_values_3d_func(x, k)
+
+        # check the value of the array view
+        assert np.all(dpnp.asnumpy(v) == x)
+        # check the value of the original arrays
+        assert np.all(dpnp.asnumpy(u) == t)
diff --git a/numba_dpex/tests/experimental/test_supported_array_types_as_kernel_args.py b/numba_dpex/tests/experimental/test_supported_array_types_as_kernel_args.py
new file mode 100644
index 0000000000..7520a37373
--- /dev/null
+++ b/numba_dpex/tests/experimental/test_supported_array_types_as_kernel_args.py
@@ -0,0 +1,39 @@
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Tests different input array type support for the kernel."""
+
+import dpctl.tensor as dpt
+import dpnp
+import pytest
+
+import numba_dpex as dpex
+import numba_dpex.experimental as dpex_exp
+from numba_dpex.tests._helper import get_all_dtypes
+
+list_of_dtypes = get_all_dtypes(
+    no_bool=True, no_float16=True, no_none=True, no_complex=True
+)
+
+zeros_func = (dpt.zeros, dpnp.zeros)
+
+_SIZE = 10
+
+
+@pytest.fixture(params=((a, b) for a in zeros_func for b in list_of_dtypes))
+def input_array(request):
+    zeros, dtype = request.param
+    return zeros(_SIZE, dtype=dtype)
+
+
+@dpex_exp.kernel
+def set_ones(a):
+    i = dpex.get_global_id(0)
+    a[i] = 1
+
+
+def test_fetch_add(input_array):
+    dpex_exp.call_kernel(set_ones, dpex.Range(_SIZE), input_array)
+
+    assert input_array[0] == 1
diff --git a/numba_dpex/tests/experimental/test_target_specific_overload.py b/numba_dpex/tests/experimental/test_target_specific_overload.py
new file mode 100644
index 0000000000..b2c773b136
--- /dev/null
+++ b/numba_dpex/tests/experimental/test_target_specific_overload.py
@@ -0,0 +1,68 @@
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import dpnp
+from numba.core.extending import overload
+
+import numba_dpex as dpex
+import numba_dpex.experimental as dpex_exp
+from numba_dpex.core.descriptor import dpex_kernel_target
+from numba_dpex.experimental.target import (
+    DPEX_KERNEL_EXP_TARGET_NAME,
+    dpex_exp_kernel_target,
+)
+
+
+def scalar_add(a, b):
+    return a + b
+
+
+@overload(scalar_add, target=DPEX_KERNEL_EXP_TARGET_NAME)
+def _ol_scalar_add(a, b):
+    def ol_scalar_add_impl(a, b):
+        return a + b
+
+    return ol_scalar_add_impl
+
+
+@dpex_exp.kernel
+def kernel_calling_overload(a, b, c):
+    i = dpex.get_global_id(0)
+    c[i] = scalar_add(a[i], b[i])
+
+
+a = dpnp.ones(10, dtype=dpnp.int64)
+b = dpnp.ones(10, dtype=dpnp.int64)
+c = dpnp.zeros(10, dtype=dpnp.int64)
+
+dpex_exp.call_kernel(kernel_calling_overload, dpex.Range(10), a, b, c)
+
+
+def test_end_to_end_overload_execution():
+    """Tests that an overload function can be called from an experimental.kernel
+    decorated function and works end to end.
+    """
+    for i in range(c.shape[0]):
+        assert c[i] == scalar_add(a[i], b[i])
+
+
+def test_overload_registration():
+    """Tests that the overload _ol_scalar_add is registered only in the
+    "dpex_kernel_exp" target and not in the "dpex_kernel" target.
+    """
+
+    def check_for_overload_registration(targetctx, key):
+        found_key = False
+        for fn_key in targetctx._defns.keys():
+            if isinstance(fn_key, str) and fn_key.startswith(key):
+                found_key = True
+                break
+        return found_key
+
+    assert check_for_overload_registration(
+        dpex_exp_kernel_target.target_context, "_ol_scalar_add"
+    )
+    assert not check_for_overload_registration(
+        dpex_kernel_target.target_context, "_ol_scalar_add"
+    )
diff --git a/numba_dpex/tests/kernel_tests/__init__.py b/numba_dpex/tests/kernel_tests/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/kernel_tests/__init__.py
+++ b/numba_dpex/tests/kernel_tests/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/kernel_tests/test_atomic_op.py b/numba_dpex/tests/kernel_tests/test_atomic_op.py
index e80614c158..0d65fc5020 100644
--- a/numba_dpex/tests/kernel_tests/test_atomic_op.py
+++ b/numba_dpex/tests/kernel_tests/test_atomic_op.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -56,7 +56,7 @@ def f(a):
 def test_kernel_atomic_simple(input_arrays, kernel_result_pair):
     a, dtype = input_arrays()
     kernel, expected = kernel_result_pair
-    kernel[dpex.Range(global_size)](a)
+    dpex.call_kernel(kernel, dpex.Range(global_size), a)
     assert a[0] == expected
 
 
@@ -96,7 +96,7 @@ def test_kernel_atomic_local(input_arrays, return_list_of_op):
     op_type, expected = return_list_of_op
     f = get_func_local(op_type, dtype)
     kernel = dpex.kernel(f)
-    kernel[dpex.Range(N), dpex.Range(N)](a)
+    dpex.call_kernel(kernel, dpex.NdRange(dpex.Range(N), dpex.Range(N)), a)
     assert a[0] == expected
 
 
@@ -134,7 +134,7 @@ def test_kernel_atomic_multi_dim(
     dim = return_list_of_dim
     kernel = get_kernel_multi_dim(op_type, len(dim))
     a = np.zeros(dim, dtype=return_dtype)
-    kernel[dpex.Range(global_size)](a)
+    dpex.call_kernel(kernel, dpex.Range(global_size), a)
     assert a[0] == expected
 
 
diff --git a/numba_dpex/tests/kernel_tests/test_barrier.py b/numba_dpex/tests/kernel_tests/test_barrier.py
index 92b8c604ed..2e7f67081a 100644
--- a/numba_dpex/tests/kernel_tests/test_barrier.py
+++ b/numba_dpex/tests/kernel_tests/test_barrier.py
@@ -1,14 +1,12 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-import dpctl
 import dpctl.tensor as dpt
 import numpy as np
-import pytest
 
 import numba_dpex as dpex
-from numba_dpex import float32, usm_ndarray, void
+from numba_dpex import NdRange, Range, float32, usm_ndarray, void
 
 f32arrty = usm_ndarray(ndim=1, dtype=float32, layout="C")
 
@@ -27,7 +25,7 @@ def twice(A):
     orig = dpt.asnumpy(arr)
     global_size = (N,)
     local_size = (N // 2,)
-    twice[global_size, local_size](arr)
+    dpex.call_kernel(twice, NdRange(global_size, local_size), arr)
     after = dpt.asnumpy(arr)
     # The computation is correct?
     np.testing.assert_allclose(orig * 2, after)
@@ -45,7 +43,7 @@ def twice(A):
     N = 256
     arr = dpt.arange(N, dtype=dpt.float32)
     orig = dpt.asnumpy(arr)
-    twice[N](arr)
+    dpex.call_kernel(twice, Range(N), arr)
     after = dpt.asnumpy(arr)
     # The computation is correct?
     np.testing.assert_allclose(orig * 2, after)
@@ -68,7 +66,9 @@ def reverse_array(A):
 
     arr = dpt.arange(blocksize, dtype=dpt.float32)
     orig = dpt.asnumpy(arr)
-    reverse_array[(blocksize,), (blocksize,)](arr)
+    dpex.call_kernel(
+        reverse_array, NdRange(Range(blocksize), Range(blocksize)), arr
+    )
     after = dpt.asnumpy(arr)
     expected = orig[::-1] + orig
     np.testing.assert_allclose(expected, after)
diff --git a/numba_dpex/tests/kernel_tests/test_caching.py b/numba_dpex/tests/kernel_tests/test_caching.py
index 4e1c6e31a4..12420a352f 100644
--- a/numba_dpex/tests/kernel_tests/test_caching.py
+++ b/numba_dpex/tests/kernel_tests/test_caching.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -6,7 +6,6 @@
 
 import dpctl.tensor as dpt
 import numpy as np
-import pytest
 
 import numba_dpex as dpex
 from numba_dpex.core.caching import LRUCache
@@ -130,7 +129,7 @@ def data_parallel_sum(x, y, z):
 
     d = JitKernel(data_parallel_sum)
 
-    d_launcher = d[100]
+    d_launcher = d[dpex.Range(100)]
 
     N = 10
     for i in range(N):
diff --git a/numba_dpex/tests/kernel_tests/test_complex_array.py b/numba_dpex/tests/kernel_tests/test_complex_array.py
index d71e434d57..2e47d43719 100644
--- a/numba_dpex/tests/kernel_tests/test_complex_array.py
+++ b/numba_dpex/tests/kernel_tests/test_complex_array.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -48,7 +48,7 @@ def test_numeric_kernel_arg_complex_scalar(input_arrays):
     a, b, _ = input_arrays
     s = a.dtype.type(2 + 1j)
 
-    kernel_scalar[dpex.Range(N)](a, b, s)
+    dpex.call_kernel(kernel_scalar, dpex.Range(N), a, b, s)
 
     nb = dpnp.asnumpy(b)
     nexpected = numpy.full_like(nb, fill_value=2 + 1j)
@@ -65,7 +65,7 @@ def test_numeric_kernel_arg_complex_array(input_arrays):
 
     a, b, c = input_arrays
 
-    kernel_array[dpex.Range(N)](a, b, c)
+    dpex.call_kernel(kernel_array, dpex.Range(N), a, b, c)
 
     nb = dpnp.asnumpy(b)
     nexpected = numpy.full_like(nb, fill_value=0 + 0j)
diff --git a/numba_dpex/tests/kernel_tests/test_dpnp_ndarray_args.py b/numba_dpex/tests/kernel_tests/test_dpnp_ndarray_args.py
index da9229bd40..cd8f06ccb6 100644
--- a/numba_dpex/tests/kernel_tests/test_dpnp_ndarray_args.py
+++ b/numba_dpex/tests/kernel_tests/test_dpnp_ndarray_args.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -85,7 +85,9 @@ def test_setting_private_from_dpnp_ndarray():
     global_range = ndpx.Range(N_POINTS // N_POINTS_PER_WORK_ITEM)
     local_range = ndpx.Range(LOCAL_SIZE)
     try:
-        _kernel[ndpx.NdRange(global_range, local_range)](COEFFICIENTS)
+        ndpx.call_kernel(
+            _kernel, ndpx.NdRange(global_range, local_range), COEFFICIENTS
+        )
     except Exception as e:
         assert (
             False
diff --git a/numba_dpex/tests/kernel_tests/test_dump_kernel_llvm.py b/numba_dpex/tests/kernel_tests/test_dump_kernel_llvm.py
index 96e962e7e1..6535f388cf 100644
--- a/numba_dpex/tests/kernel_tests/test_dump_kernel_llvm.py
+++ b/numba_dpex/tests/kernel_tests/test_dump_kernel_llvm.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -8,7 +8,8 @@
 import os
 
 import numba_dpex as dpex
-from numba_dpex import config, float32, usm_ndarray
+from numba_dpex import float32, usm_ndarray
+from numba_dpex.core import config
 from numba_dpex.core.descriptor import dpex_kernel_target
 
 f32arrty = usm_ndarray(ndim=1, dtype=float32, layout="C")
diff --git a/numba_dpex/tests/kernel_tests/test_func.py b/numba_dpex/tests/kernel_tests/test_func.py
index 6fc0226f30..75ad6b1f23 100644
--- a/numba_dpex/tests/kernel_tests/test_func.py
+++ b/numba_dpex/tests/kernel_tests/test_func.py
@@ -1,8 +1,7 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-
 import dpnp
 import numpy
 
@@ -11,7 +10,7 @@
 
 @dpex.func
 def g(a):
-    return a + 1
+    return a + dpnp.float32(1)
 
 
 @dpex.kernel
@@ -24,6 +23,6 @@ def test_func_call_from_kernel():
     a = dpnp.ones(1024)
     b = dpnp.ones(1024)
 
-    f[dpex.Range(1024)](a, b)
+    dpex.call_kernel(f, dpex.Range(1024), a, b)
     nb = dpnp.asnumpy(b)
     assert numpy.all(nb == 2)
diff --git a/numba_dpex/tests/kernel_tests/test_func_qualname_disambiguation.py b/numba_dpex/tests/kernel_tests/test_func_qualname_disambiguation.py
index 507fef3740..0a852413c7 100644
--- a/numba_dpex/tests/kernel_tests/test_func_qualname_disambiguation.py
+++ b/numba_dpex/tests/kernel_tests/test_func_qualname_disambiguation.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/kernel_tests/test_func_specialization.py b/numba_dpex/tests/kernel_tests/test_func_specialization.py
index 386535ba18..e44af1b7c3 100644
--- a/numba_dpex/tests/kernel_tests/test_func_specialization.py
+++ b/numba_dpex/tests/kernel_tests/test_func_specialization.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2023 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -17,7 +17,7 @@
 
 
 def increment(a):
-    return a + 1
+    return a + dpnp.float32(1)
 
 
 def test_basic():
@@ -35,7 +35,7 @@ def kernel_function(a, b):
     a = dpnp.ones(N)
     b = dpnp.ones(N)
 
-    k[dpex.Range(N)](a, b)
+    dpex.call_kernel(k, dpex.Range(N), a, b)
 
     assert np.array_equal(dpnp.asnumpy(b), dpnp.asnumpy(a) + 1)
 
@@ -56,7 +56,7 @@ def kernel_function(a, b):
     a = dpnp.ones(N, dtype=dpnp.int32)
     b = dpnp.ones(N, dtype=dpnp.int32)
 
-    k[dpex.Range(N)](a, b)
+    dpex.call_kernel(k, dpex.Range(N), a, b)
 
     assert np.array_equal(dpnp.asnumpy(b), dpnp.asnumpy(a) + 1)
 
@@ -65,7 +65,7 @@ def kernel_function(a, b):
     b = dpnp.ones(N, dtype=dpnp.int64)
 
     with pytest.raises(Exception) as e:
-        k[dpex.Range(N)](a, b)
+        dpex.call_kernel(k, dpex.Range(N), a, b)
 
     assert " >>> <unknown function>(int64)" in e.value.args[0]
 
@@ -86,7 +86,7 @@ def kernel_function(a, b):
     a = dpnp.ones(N, dtype=dpnp.int32)
     b = dpnp.ones(N, dtype=dpnp.int32)
 
-    k[dpex.Range(N)](a, b)
+    dpex.call_kernel(k, dpex.Range(N), a, b)
 
     assert np.array_equal(dpnp.asnumpy(b), dpnp.asnumpy(a) + 1)
 
@@ -94,7 +94,7 @@ def kernel_function(a, b):
     a = dpnp.ones(N, dtype=dpnp.float32)
     b = dpnp.ones(N, dtype=dpnp.float32)
 
-    k[dpex.Range(N)](a, b)
+    dpex.call_kernel(k, dpex.Range(N), a, b)
 
     assert np.array_equal(dpnp.asnumpy(b), dpnp.asnumpy(a) + 1)
 
@@ -103,6 +103,6 @@ def kernel_function(a, b):
     b = dpnp.ones(N, dtype=dpnp.int64)
 
     with pytest.raises(Exception) as e:
-        k[dpex.Range(N)](a, b)
+        dpex.call_kernel(k, dpex.Range(N), a, b)
 
     assert " >>> <unknown function>(int64)" in e.value.args[0]
diff --git a/numba_dpex/tests/kernel_tests/test_invalid_kernel_args.py b/numba_dpex/tests/kernel_tests/test_invalid_kernel_args.py
index 7cea27c5c4..7a750c8a56 100644
--- a/numba_dpex/tests/kernel_tests/test_invalid_kernel_args.py
+++ b/numba_dpex/tests/kernel_tests/test_invalid_kernel_args.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -26,4 +26,4 @@ def test_passing_numpy_arrays_as_kernel_args():
     c = numpy.zeros(N)
 
     with pytest.raises(UnsupportedKernelArgumentError):
-        vecadd_kernel[dpex.Range(N)](a, b, c)
+        dpex.call_kernel(vecadd_kernel, dpex.Range(N), a, b, c)
diff --git a/numba_dpex/tests/kernel_tests/test_kernel_has_return_value_error.py b/numba_dpex/tests/kernel_tests/test_kernel_has_return_value_error.py
index 60a403ea2f..1d90b88b2c 100644
--- a/numba_dpex/tests/kernel_tests/test_kernel_has_return_value_error.py
+++ b/numba_dpex/tests/kernel_tests/test_kernel_has_return_value_error.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -31,4 +31,4 @@ def test_return(sig):
 
     with pytest.raises(dpex.core.exceptions.KernelHasReturnValueError):
         kernel_fn = dpex.kernel(sig)(f)
-        kernel_fn[dpex.Range(a.size)](a)
+        dpex.call_kernel(kernel_fn, dpex.Range(a.size), a)
diff --git a/numba_dpex/tests/kernel_tests/test_kernel_launch_params.py b/numba_dpex/tests/kernel_tests/test_kernel_launch_params.py
index 55e383e44c..6505401030 100644
--- a/numba_dpex/tests/kernel_tests/test_kernel_launch_params.py
+++ b/numba_dpex/tests/kernel_tests/test_kernel_launch_params.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -21,98 +21,18 @@ def vecadd(a, b, c):
     a[i] = b[i] + c[i]
 
 
-def test_1D_global_range_as_int():
-    with pytest.deprecated_call():
-        k = vecadd[10]
-        assert k._global_range == [10]
-        assert k._local_range is None
-
-
 def test_1D_global_range_as_one_tuple():
     k = vecadd[Range(10)]
     assert k._global_range == [10]
     assert k._local_range is None
 
 
-def test_1D_global_range_as_list():
-    with pytest.deprecated_call():
-        k = vecadd[[10]]
-        assert k._global_range == [10]
-        assert k._local_range is None
-
-
-def test_1D_global_range_and_1D_local_range1():
-    with pytest.deprecated_call():
-        k = vecadd[[10, 10]]
-        assert k._global_range == [10]
-        assert k._local_range == [10]
-
-
-def test_1D_global_range_and_1D_local_range2():
-    with pytest.deprecated_call():
-        k = vecadd[(10,), (10,)]
-        assert k._global_range == [10]
-        assert k._local_range == [10]
-
-
-def test_2D_global_range_and_2D_local_range1():
-    with pytest.deprecated_call():
-        k = vecadd[(10, 10), (10, 10)]
-        assert k._global_range == [10, 10]
-        assert k._local_range == [10, 10]
-
-
-def test_2D_global_range_and_2D_local_range2():
-    with pytest.deprecated_call():
-        k = vecadd[[10, 10], (10, 10)]
-        assert k._global_range == [10, 10]
-        assert k._local_range == [10, 10]
-
-
-def test_2D_global_range_and_2D_local_range3():
-    with pytest.deprecated_call():
-        k = vecadd[(10, 10), [10, 10]]
-        assert k._global_range == [10, 10]
-        assert k._local_range == [10, 10]
-
-
 def test_2D_global_range_and_2D_local_range4():
     k = vecadd[dpex.NdRange((10, 10), (10, 10))]
     assert k._global_range == [10, 10]
     assert k._local_range == [10, 10]
 
 
-def test_deprecation_warning_for_empty_local_range1():
-    with pytest.deprecated_call():
-        k = vecadd[[10, 10], []]
-    assert k._global_range == [10, 10]
-    assert k._local_range is None
-
-
-def test_deprecation_warning_for_empty_local_range2():
-    with pytest.deprecated_call():
-        k = vecadd[10, []]
-    assert k._global_range == [10]
-    assert k._local_range is None
-
-
-def test_ambiguous_kernel_launch_params():
-    with pytest.deprecated_call():
-        k = vecadd[10, 10]
-    assert k._global_range == [10]
-    assert k._local_range == [10]
-
-    with pytest.deprecated_call():
-        k = vecadd[(10, 10)]
-    assert k._global_range == [10]
-    assert k._local_range == [10]
-
-    with pytest.deprecated_call():
-        k = vecadd[((10), (10))]
-    assert k._global_range == [10]
-    assert k._local_range == [10]
-
-
 def test_unknown_global_range_error():
     device = dpctl.select_default_device()
     a = dpt.ones(10, dtype=dpt.int16, device=device)
@@ -124,35 +44,5 @@ def test_unknown_global_range_error():
         assert "No global range" in e.message
 
 
-def test_illegal_kernel_launch_arg1():
-    with pytest.raises(InvalidKernelLaunchArgsError):
-        with pytest.deprecated_call():
-            vecadd[()]
-
-
-def test_illegal_kernel_launch_arg2():
-    with pytest.raises(InvalidKernelLaunchArgsError):
-        with pytest.deprecated_call():
-            vecadd[10, 10, []]
-
-
-def test_illegal_range_error1():
-    with pytest.raises(IllegalRangeValueError):
-        with pytest.deprecated_call():
-            vecadd[[], []]
-
-
-def test_illegal_range_error2():
-    with pytest.raises(IllegalRangeValueError):
-        with pytest.deprecated_call():
-            vecadd[[], 10]
-
-
-def test_illegal_range_error3():
-    with pytest.raises(IllegalRangeValueError):
-        with pytest.deprecated_call():
-            vecadd[(), 10]
-
-
 if __name__ == "__main__":
     test_unknown_global_range_error()
diff --git a/numba_dpex/tests/kernel_tests/test_kernel_specialization.py b/numba_dpex/tests/kernel_tests/test_kernel_specialization.py
index afc189fb86..276c8fec0f 100644
--- a/numba_dpex/tests/kernel_tests/test_kernel_specialization.py
+++ b/numba_dpex/tests/kernel_tests/test_kernel_specialization.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2022 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2022 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -60,7 +60,9 @@ def test_missing_specialization_error():
     c = dpt.zeros(1024, dtype=dpt.int32)
 
     with pytest.raises(MissingSpecializationError):
-        specialized_kernel1(data_parallel_sum)[Range(1024)](a, b, c)
+        dpex.call_kernel(
+            specialized_kernel1(data_parallel_sum), Range(1024), a, b, c
+        )
 
 
 def test_execution_of_specialized_kernel():
@@ -69,7 +71,9 @@ def test_execution_of_specialized_kernel():
     b = dpt.ones(1024, dtype=dpt.int64)
     c = dpt.zeros(1024, dtype=dpt.int64)
 
-    specialized_kernel1(data_parallel_sum)[Range(1024)](a, b, c)
+    dpex.call_kernel(
+        specialized_kernel1(data_parallel_sum), Range(1024), a, b, c
+    )
 
     npc = dpt.asnumpy(c)
     import numpy as np
diff --git a/numba_dpex/tests/kernel_tests/test_math_functions.py b/numba_dpex/tests/kernel_tests/test_math_functions.py
index 24ad66bd02..99afb1c0e8 100644
--- a/numba_dpex/tests/kernel_tests/test_math_functions.py
+++ b/numba_dpex/tests/kernel_tests/test_math_functions.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -43,7 +43,7 @@ def f(a, b):
         i = dpex.get_global_id(0)
         b[i] = uop(a[i])
 
-    f[dpex.Range(a.size)](a, b)
+    dpex.call_kernel(f, dpex.Range(a.size), a, b)
 
     expected = dpnp_uop(a)
 
diff --git a/numba_dpex/tests/kernel_tests/test_ndrange_exceptions.py b/numba_dpex/tests/kernel_tests/test_ndrange_exceptions.py
index 6c1320b061..079cc551b3 100644
--- a/numba_dpex/tests/kernel_tests/test_ndrange_exceptions.py
+++ b/numba_dpex/tests/kernel_tests/test_ndrange_exceptions.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -34,4 +34,4 @@ def test_ndrange_config_error(error, ranges):
 
     with pytest.raises(error):
         range = NdRange(ranges[0], ranges[1])
-        kernel_vector_sum[range](a, b, c)
+        ndpx.call_kernel(kernel_vector_sum, range, a, b, c)
diff --git a/numba_dpex/tests/kernel_tests/test_print.py b/numba_dpex/tests/kernel_tests/test_print.py
index 25402421fc..5de65e467f 100644
--- a/numba_dpex/tests/kernel_tests/test_print.py
+++ b/numba_dpex/tests/kernel_tests/test_print.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -42,7 +42,7 @@ def print_scalar_val(s):
 
     a = input_arrays
 
-    print_scalar_val[dpex.Range(1)](a)
+    dpex.call_kernel(print_scalar_val, dpex.Range(1), a)
     captured = capfd.readouterr()
     assert "printing ... 10" in captured.out
 
@@ -60,7 +60,7 @@ def print_scalar_val(s):
 
     a = input_arrays
 
-    print_scalar_val[dpex.Range(1)](a)
+    dpex.call_kernel(print_scalar_val, dpex.Range(1), a)
     captured = capfd.readouterr()
 
     assert "10" in captured.out
@@ -85,7 +85,7 @@ def print_string(a):
     a = input_arrays
 
     with pytest.raises(LoweringError):
-        print_string[dpex.Range(1)](a)
+        dpex.call_kernel(print_string, dpex.Range(1), a)
 
 
 @skip_on_gpu
@@ -101,4 +101,4 @@ def print_string(a):
     a = input_arrays
 
     with pytest.raises(LoweringError):
-        print_string[dpex.Range(1)](a)
+        dpex.call_kernel(print_string, dpex.Range(1), a)
diff --git a/numba_dpex/tests/kernel_tests/test_private_memory_allocation.py b/numba_dpex/tests/kernel_tests/test_private_memory_allocation.py
index 9d12602048..f806ebaa56 100644
--- a/numba_dpex/tests/kernel_tests/test_private_memory_allocation.py
+++ b/numba_dpex/tests/kernel_tests/test_private_memory_allocation.py
@@ -1,8 +1,7 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
-
 import dpnp
 import numpy
 
@@ -23,7 +22,7 @@ def kernel_with_private_memory_allocation(A):
 def test_private_memory_allocation():
     N = 64
     arr = dpnp.zeros(N, dtype=dpnp.float32)
-    kernel_with_private_memory_allocation[dpex.Range(N)](arr)
+    dpex.call_kernel(kernel_with_private_memory_allocation, dpex.Range(N), arr)
 
     nparr = dpnp.asnumpy(arr)
 
diff --git a/numba_dpex/tests/kernel_tests/test_scalar_arg_types.py b/numba_dpex/tests/kernel_tests/test_scalar_arg_types.py
index 8b1fb576cd..7cfac6cf27 100644
--- a/numba_dpex/tests/kernel_tests/test_scalar_arg_types.py
+++ b/numba_dpex/tests/kernel_tests/test_scalar_arg_types.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -49,7 +49,7 @@ def test_numeric_kernel_arg_types1(input_arrays):
     a, b = input_arrays
     s = a.dtype.type(2)
 
-    scaling_kernel[dpex.Range(N)](a, b, s)
+    dpex.call_kernel(scaling_kernel, dpex.Range(N), a, b, s)
 
     nb = dpnp.asnumpy(b)
     nexpected = numpy.full_like(nb, fill_value=2)
@@ -65,14 +65,14 @@ def test_bool_kernel_arg_type(input_arrays):
     """
     a, b = input_arrays
 
-    kernel_with_bool_arg[dpex.Range(a.size)](a, b, True)
+    dpex.call_kernel(kernel_with_bool_arg, dpex.Range(a.size), a, b, True)
 
     nb = dpnp.asnumpy(b)
     nexpected_true = numpy.full_like(nb, fill_value=2)
 
     assert numpy.allclose(nb, nexpected_true)
 
-    kernel_with_bool_arg[dpex.Range(a.size)](a, b, False)
+    dpex.call_kernel(kernel_with_bool_arg, dpex.Range(a.size), a, b, False)
 
     nb = dpnp.asnumpy(b)
     nexpected_false = numpy.zeros_like(nb)
diff --git a/numba_dpex/tests/kernel_tests/test_sycl_usm_array_iface_interop.py b/numba_dpex/tests/kernel_tests/test_sycl_usm_array_iface_interop.py
index bd89265bb7..908350858d 100644
--- a/numba_dpex/tests/kernel_tests/test_sycl_usm_array_iface_interop.py
+++ b/numba_dpex/tests/kernel_tests/test_sycl_usm_array_iface_interop.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -88,7 +88,7 @@ def test_kernel_valid_usm_obj(dtype):
     C = DuckUSMArray(shape=buffC.shape, dtype=dtype, host_buffer=buffC)
 
     try:
-        vecadd[dpex.Range(N)](A, B, C)
+        dpex.call_kernel(vecadd, dpex.Range(N), A, B, C)
     except Exception:
         pytest.fail(
             "Could not pass Python object with sycl_usm_array_interface"
@@ -112,4 +112,4 @@ def test_kernel_invalid_usm_obj(dtype):
     C = PseudoDuckUSMArray()
 
     with pytest.raises(Exception):
-        vecadd[dpex.Range(N)](A, B, C)
+        dpex.call_kernel(vecadd, dpex.Range(N), A, B, C)
diff --git a/numba_dpex/tests/kernel_tests/test_usm_ndarray_interop.py b/numba_dpex/tests/kernel_tests/test_usm_ndarray_interop.py
index a11a9d7e15..e6863e47e5 100644
--- a/numba_dpex/tests/kernel_tests/test_usm_ndarray_interop.py
+++ b/numba_dpex/tests/kernel_tests/test_usm_ndarray_interop.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -9,12 +9,6 @@
 import numba_dpex as dpex
 from numba_dpex.tests._helper import get_all_dtypes
 
-# list_of_dtype = [
-#     numpy.int32,
-#     numpy.int64,
-#     numpy.float32,
-#     numpy.float64,
-# ]
 list_of_dtype = get_all_dtypes(
     no_bool=True, no_float16=True, no_none=True, no_complex=True
 )
@@ -58,7 +52,7 @@ def data_parallel_sum(a, b, c):
 
     c = dpt.empty_like(a)
 
-    data_parallel_sum[dpex.Range(N, N)](a, b, c)
+    dpex.call_kernel(data_parallel_sum, dpex.Range(N, N), a, b, c)
 
     na = dpt.asnumpy(a)
     nb = dpt.asnumpy(b)
diff --git a/numba_dpex/tests/misc/__init__.py b/numba_dpex/tests/misc/__init__.py
index 141a9eccf2..fa6c31ef7c 100644
--- a/numba_dpex/tests/misc/__init__.py
+++ b/numba_dpex/tests/misc/__init__.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/misc/test_dpctl_version.py b/numba_dpex/tests/misc/test_dpctl_version.py
index b055817c87..c4c9b81e1d 100644
--- a/numba_dpex/tests/misc/test_dpctl_version.py
+++ b/numba_dpex/tests/misc/test_dpctl_version.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/misc/test_parse_sem_version.py b/numba_dpex/tests/misc/test_parse_sem_version.py
index 8d39754bc1..82d6492273 100644
--- a/numba_dpex/tests/misc/test_parse_sem_version.py
+++ b/numba_dpex/tests/misc/test_parse_sem_version.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/misc/test_warnings.py b/numba_dpex/tests/misc/test_warnings.py
new file mode 100644
index 0000000000..2a36695784
--- /dev/null
+++ b/numba_dpex/tests/misc/test_warnings.py
@@ -0,0 +1,62 @@
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import warnings
+
+import dpnp
+import pytest
+
+import numba_dpex as dpex
+from numba_dpex.core import config
+
+
+@dpex.kernel(enable_cache=False)
+def foo(a):
+    a[dpex.get_global_id(0)] = 0
+
+
+def test_opt_warning():
+    bkp = config.DPEX_OPT
+    config.DPEX_OPT = 3
+
+    with pytest.warns(UserWarning):
+        dpex.call_kernel(foo, dpex.Range(10), dpnp.arange(10))
+
+    config.DPEX_OPT = bkp
+
+
+def test_inline_threshold_eq_3_warning():
+    bkp = config.INLINE_THRESHOLD
+    config.INLINE_THRESHOLD = 3
+
+    with pytest.warns(UserWarning):
+        dpex.call_kernel(foo, dpex.Range(10), dpnp.arange(10))
+
+    config.INLINE_THRESHOLD = bkp
+
+
+def test_inline_threshold_negative_val_warning_():
+    bkp = config.INLINE_THRESHOLD
+    config.INLINE_THRESHOLD = -1
+
+    with pytest.warns(UserWarning):
+        dpex.call_kernel(foo, dpex.Range(10), dpnp.arange(10))
+
+    config.INLINE_THRESHOLD = bkp
+
+
+def test_inline_threshold_gt_3_warning():
+    bkp = config.INLINE_THRESHOLD
+    config.INLINE_THRESHOLD = 4
+
+    with pytest.warns(UserWarning):
+        dpex.call_kernel(foo, dpex.Range(10), dpnp.arange(10))
+
+    config.INLINE_THRESHOLD = bkp
+
+
+def test_no_warning():
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")
+        dpex.call_kernel(foo, dpex.Range(10), dpnp.arange(10))
diff --git a/numba_dpex/tests/test_array_utils.py b/numba_dpex/tests/test_array_utils.py
index bf8590a585..d51a19cece 100644
--- a/numba_dpex/tests/test_array_utils.py
+++ b/numba_dpex/tests/test_array_utils.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/test_debuginfo.py b/numba_dpex/tests/test_debuginfo.py
index 679bf53579..140b46a37a 100644
--- a/numba_dpex/tests/test_debuginfo.py
+++ b/numba_dpex/tests/test_debuginfo.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/test_dpex_use_alongside_numba.py b/numba_dpex/tests/test_dpex_use_alongside_numba.py
new file mode 100644
index 0000000000..526afadd8d
--- /dev/null
+++ b/numba_dpex/tests/test_dpex_use_alongside_numba.py
@@ -0,0 +1,40 @@
+# SPDX-FileCopyrightText: 2024 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+This module contains tests to ensure that numba.njit works with numpy after
+importing numba_dpex. Aka lazy testing if we break numba's default behavior.
+"""
+
+import numba as nb
+import numpy as np
+
+import numba_dpex
+
+
+@nb.njit
+def add1(a):
+    return a + 1
+
+
+def add_py(a, b):
+    return np.add(a, b)
+
+
+add_jit = nb.njit(add_py)
+
+
+def test_add1():
+    a = np.asarray([1j], dtype=np.complex64)
+    assert np.array_equal(add1(a), np.asarray([1 + 1j], dtype=np.complex64))
+
+
+def test_add_py():
+    a = np.ones((10,), dtype=np.complex128)
+    assert np.array_equal(add_py(a, 1.5), np.full((10,), 2.5, dtype=a.dtype))
+
+
+def test_add_jit():
+    a = np.ones((10,), dtype=np.complex128)
+    assert np.array_equal(add_jit(a, 1.5), np.full((10,), 2.5, dtype=a.dtype))
diff --git a/numba_dpex/tests/test_examples.py b/numba_dpex/tests/test_examples.py
index 1549521332..3898a1c561 100644
--- a/numba_dpex/tests/test_examples.py
+++ b/numba_dpex/tests/test_examples.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
diff --git a/numba_dpex/tests/test_prange.py b/numba_dpex/tests/test_prange.py
index 4e42db8cf6..d0f5c55865 100644
--- a/numba_dpex/tests/test_prange.py
+++ b/numba_dpex/tests/test_prange.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# Copyright 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
 
@@ -12,9 +12,12 @@
 
 from numba_dpex import dpjit, prange
 
+from ._helper import decorators
 
-def test_one_prange_mul():
-    @dpjit
+
+@pytest.mark.parametrize("jit", decorators)
+def test_one_prange_mul(jit):
+    @jit
     def f(a, b):
         for i in prange(4):
             b[i, 0] = a[i, 0] * 10
@@ -35,6 +38,33 @@ def f(a, b):
         assert nb[i, 0] == na[i, 0] * 10
 
 
+@pytest.mark.parametrize("jit", decorators)
+def test_one_prange_mul_nested(jit):
+    @jit
+    def f_inner(a, b):
+        for i in prange(4):
+            b[i, 0] = a[i, 0] * 10
+        return
+
+    @jit
+    def f(a, b):
+        return f_inner(a, b)
+
+    device = dpctl.select_default_device()
+
+    m = 8
+    n = 8
+    a = dpnp.ones((m, n), device=device)
+    b = dpnp.ones((m, n), device=device)
+
+    f(a, b)
+    na = dpnp.asnumpy(a)
+    nb = dpnp.asnumpy(b)
+
+    for i in range(4):
+        assert nb[i, 0] == na[i, 0] * 10
+
+
 @pytest.mark.skip(reason="dpnp.add() doesn't support variable + scalar.")
 def test_one_prange_add_scalar():
     @dpjit
@@ -155,8 +185,9 @@ def f(a, b):
     assert np.all(b.asnumpy() == 12)
 
 
-def test_two_consecutive_prange():
-    @dpjit
+@pytest.mark.parametrize("jit", decorators)
+def test_two_consecutive_prange(jit):
+    @jit
     def prange_example(a, b, c, d):
         for i in prange(n):
             c[i] = a[i] + b[i]
diff --git a/numba_dpex/tests/test_vectorize.py b/numba_dpex/tests/test_vectorize.py
index f33593a919..ec25ce9864 100644
--- a/numba_dpex/tests/test_vectorize.py
+++ b/numba_dpex/tests/test_vectorize.py
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 
-# SPDX-FileCopyrightText: 2020 - 2023 Intel Corporation
+# SPDX-FileCopyrightText: 2020 - 2024 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0