From 4a0934d2122cabbab99423496ed4b85b3e5876aa Mon Sep 17 00:00:00 2001 From: geoffreyweal Date: Thu, 18 Jun 2026 17:33:32 +1200 Subject: [PATCH 01/10] Our documentation on Jupyter Kernels is a bit scattered and inconsistent. Using tabs to try to help the user figure out what method they need to use to get Jupyer kernels working --- .../Jupyter_kernels_Manual_management.md | 436 ------------------ ...upyter_kernels_Tool_assisted_management.md | 160 ------- 2 files changed, 596 deletions(-) delete mode 100644 docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md delete mode 100644 docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md deleted file mode 100644 index d859d4cd9..000000000 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md +++ /dev/null @@ -1,436 +0,0 @@ ---- -created_at: 2025-01-24 -description: How to set up your own custom kernels for use on Mahuika JupyterHub -tags: - - JupyterHub - - Python - - R ---- - -# Jupyter kernels - Manual management - -## Introduction - -Jupyter kernels execute the code that you write. Mahuika provides a number of -Python and R kernels by default, which can be selected from the Launcher. - -Many packages are preinstalled in our default Python and R environments -and these can be extended further as described on the -[Python](../../../../Software/Available_Applications/Python.md) and -[R](../../../../Software/Available_Applications/R.md) support -pages. - -## Adding a Custom Python kernel - -You can configure custom Python kernels for running your Jupyter -notebooks. This could be necessary and/or recommended in some -situations, including: - -- If you wish to load a different combination of environment modules - than those we load in our default kernels -- If you would like to activate a virtual environment or conda - environment before launching the kernel - -The following example will create a custom kernel based on either a conda -environment (using the Miniforge3 environment module) or a Python virtual -environment (using a Python environment module). The same approach applies -to other environment modules too. - -!!! note "see also" - See the [Jupyter kernels - Tool-assisted management](./Jupyter_kernels_Tool_assisted_management.md) - page for the **preferred** way to register kernels, which uses the - `nesi-add-kernel` command line tool to automate most of these manual - steps. - -First, change directory into the path that you would like to place your -environment. - -- **If you would like to share this environment with other users**, change directory -into your project folder using `cd /nesi/project/`. Do not use the path -that includes `00_nesi_projects` or `home` in the name as this causes issues. - -Next, you will create your environment and a wrapper script for it. You can -choose whether to use a **conda environment**, or a **Python virtual -environment**: - -=== "Conda environment" - - Second, in a terminal run the following commands to load a Miniforge - environment module: - - ``` sh - module purge - module load Miniforge3 - ``` - - Now create a conda environment named "my-conda-env" using Python 3.11. - The *ipykernel* Python package is required but you can change the names - of the environment, version of Python and install other Python packages - as required. - - ``` sh - conda create --prefix ./my-conda-env python=3.11 - source $(conda info --base)/etc/profile.d/conda.sh - conda activate ./my-conda-env - conda install ipykernel - # you can pip/conda install other packages here too - ``` - - Third, we will create a wrapper for your conda environment. - Change directory into your `my-conda-env` folder: - - ``` sh - cd my-conda-env - ``` - - And add the following as `wrapper.sh` into your `my-conda-env` folder: - - ``` sh - #!/usr/bin/env bash - - # load required modules here - module purge - module load Miniforge3 - - # activate conda environment - source $(conda info --base)/etc/profile.d/conda.sh - conda deactivate # workaround for https://github.com/conda/conda/issues/9392 - conda activate my-conda-env - - # run the kernel - exec python $@ - ``` - - Make the wrapper script executable: - - ``` sh - chmod +x wrapper.sh - ``` - - Fourth, create a Jupyter kernel based on your new conda environment: - - ``` sh - python -m ipykernel install --user --name my-conda-env --display-name="My Conda Env" - ``` - - We must now edit the kernel to load the required environment - modules before the kernel is launched. Change to the directory the - kernelspec was installed to - `~/.local/share/jupyter/kernels/my-conda-env`, (assuming you kept - `--name my-conda-env` in the above command): - - ``` sh - mkdir -p ~/.local/share/jupyter/kernels/my-conda-env - cd ~/.local/share/jupyter/kernels/my-conda-env - ``` - - and edit the *kernel.json* to change the first element of the argv list - to point to the wrapper script we just created. The file should look - like this: - - ```json - { - "argv": [ - "/wrapper.sh", - "-m", - "ipykernel_launcher", - "-f", - "{connection_file}" - ], - "display_name": "My Conda Env", - "language": "python" - } - ``` - - After refreshing JupyterLab your new kernel should show up in the - Launcher as "My Conda Env". - -=== "Python virtual environment" - - Second, in a terminal run the following commands to load a Python - environment module: - - ``` sh - module purge - module load Python/3.14.4-foss-2026 - ``` - - Now create a Python virtual environment named "my-venv". - The *ipykernel* Python package is required but you can change the name - of the environment and install other Python packages as required. - - ``` sh - python3 -m venv ./my-venv - source ./my-venv/bin/activate - pip install --upgrade pip - pip install ipykernel - # you can pip install other packages here too - ``` - - Third, we will create a wrapper for your virtual environment. - Change directory into your `my-venv` folder: - - ``` sh - cd my-venv - ``` - - And add the following as `wrapper.sh` into your `my-venv` folder: - - ``` sh - #!/usr/bin/env bash - - # load required modules here - module purge - module load Python/3.14.4-foss-2026 - - # activate virtual environment - source /my-venv/bin/activate - - # run the kernel - exec python $@ - ``` - - Make the wrapper script executable: - - ``` sh - chmod +x wrapper.sh - ``` - - Fourth, create a Jupyter kernel based on your new virtual environment: - - ``` sh - python -m ipykernel install --user --name my-venv --display-name="My Venv" - ``` - - We must now edit the kernel to load the required environment - modules before the kernel is launched. Change to the directory the - kernelspec was installed to - `~/.local/share/jupyter/kernels/my-venv`, (assuming you kept - `--name my-venv` in the above command): - - ``` sh - mkdir -p ~/.local/share/jupyter/kernels/my-venv - cd ~/.local/share/jupyter/kernels/my-venv - ``` - - and edit the *kernel.json* to change the first element of the argv list - to point to the wrapper script we just created. The file should look - like this: - - ```json - { - "argv": [ - "/wrapper.sh", - "-m", - "ipykernel_launcher", - "-f", - "{connection_file}" - ], - "display_name": "My Venv", - "language": "python" - } - ``` - - After refreshing JupyterLab your new kernel should show up in the - Launcher as "My Venv". - -## Sharing your custom kernal with your project team members - -You can also configure a shared Python kernel that others with access to -the same project will be able to load. - -- To do this, you must make sure it also exists in a shared location -(other users cannot see your home directory). - -First, **you** need to perform the steps in [Adding a Custom Python kernel](#adding-a-custom-python-kernel) - -Next, **your team members** need to set up the kernel on their side. The -exact steps depend on the type of environment you created, so follow the -steps in the tab that matches it: - -=== "Conda environment" - - Second, **your team members** need to run the following commands in the - terminal: - - ``` sh - # change directory into the path that contains your conda environment - cd - - # load Miniforge3 - module purge - module load Miniforge3 - - # Activate your shared conda environment - source $(conda info --base)/etc/profile.d/conda.sh - conda activate ./my-conda-env - ``` - - Third, get **your team members** to create a Jupyter kernel based on your - conda environment: - - ``` sh - python -m ipykernel install --user --name my-conda-env --display-name="My Conda Env" - ``` - - **Your project members** must now edit the kernel in their home directories - to load the required environment modules before the kernel is launched. - Change to the directory the kernelspec was installed to - `~/.local/share/jupyter/kernels/my-conda-env`, (assuming you kept - `--name my-conda-env` in the above command): - - ``` sh - mkdir -p ~/.local/share/jupyter/kernels/my-conda-env - cd ~/.local/share/jupyter/kernels/my-conda-env - ``` - - and edit the *kernel.json* to change the first element of the argv list - to point to the wrapper script we just created. The file should look - like this: - - ```json - { - "argv": [ - "/wrapper.sh", - "-m", - "ipykernel_launcher", - "-f", - "{connection_file}" - ], - "display_name": "My Conda Env", - "language": "python" - } - ``` - - After refreshing JupyterLab your new kernel should show up in the - Launcher as "My Conda Env". - -=== "Python virtual environment" - - Second, **your team members** need to run the following commands in the - terminal: - - ``` sh - # load the Python environment module - module purge - module load Python/3.14.4-foss-2026 - - # Activate the shared virtual environment - source /my-venv/bin/activate - ``` - - Third, get **your team members** to create a Jupyter kernel based on your - virtual environment: - - ``` sh - python -m ipykernel install --user --name my-venv --display-name="My Venv" - ``` - - **Your project members** must now edit the kernel in their home directories - to load the required environment modules before the kernel is launched. - Change to the directory the kernelspec was installed to - `~/.local/share/jupyter/kernels/my-venv`, (assuming you kept - `--name my-venv` in the above command): - - ``` sh - mkdir -p ~/.local/share/jupyter/kernels/my-venv - cd ~/.local/share/jupyter/kernels/my-venv - ``` - - and edit the *kernel.json* to change the first element of the argv list - to point to the wrapper script we just created. The file should look - like this: - - ```json - { - "argv": [ - "/wrapper.sh", - "-m", - "ipykernel_launcher", - "-f", - "{connection_file}" - ], - "display_name": "My Venv", - "language": "python" - } - ``` - - After refreshing JupyterLab your new kernel should show up in the - Launcher as "My Venv". - -## Custom kernel in a Singularity container - -An example showing setting up a custom kernel running in a Singularity -container can be found on our [Lambda Stack](../../../../Software/Available_Applications/Lambda_Stack.md#lambda-stack-via-jupyter) -support page. - -## Adding a custom R kernel - -You can configure custom R kernels for running your Jupyter notebooks. -The following example will create a custom kernel based on the -R/3.6.2-gimkl-2020a environment module and will additionally load an -MPFR environment module (e.g. if you wanted to load the Rmpfr package). - -In a terminal run the following commands to load the required -environment modules: - -``` sh -module purge -module load IRkernel/1.1.1-gimkl-2020a-R-3.6.2 -module load Python/3.8.2-gimkl-2020a -``` - -The IRkernel module loads the R module as a dependency and provides the -R kernel for Jupyter. Python is required to install the kernel (since -Jupyter is written in Python). - -Now create an R Jupyter kernel based on your new conda environment: - -``` sh -R -e "IRkernel::installspec(name='myrwithmpfr', displayname = 'R with MPFR', user = TRUE)" -``` - -We must now to edit the kernel to load the required environment -modules when the kernel is launched. Change to the directory the -kernelspec was installed to -(~/.local/share/jupyter/kernels/myrwithmpfr, assuming you kept `--name -myrwithmpfr` in the above command): - -``` sh -cd ~/.local/share/jupyter/kernels/myrwithmpfr -``` - -Now create a wrapper script in that directory, called *wrapper.sh*, with -the following contents: - -``` sh -#!/usr/bin/env bash - -# load required modules here -module purge -module load MPFR/4.0.2-GCCcore-9.2.0 -module load IRkernel/1.1.1-gimkl-2020a-R-3.6.2 - -# run the kernel -exec R $@ -``` - -Make the wrapper script executable: - -``` sh -chmod +x wrapper.sh_ - "argv": [ - "/home//.local/share/jupyter/kernels/myrwithmpfr/wrapper.sh", - "--slave", - "-e", - "IRkernel::main()", - "--args", - "{connection_file}" - ], - "display_name": "R with MPFR", - "language": "R" -} -``` - -After refreshing JupyterLab your new R kernel should show up in the -Launcher as "R with MPFR". diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md deleted file mode 100644 index 3b7d8b87e..000000000 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -title: Jupyter kernels - Tool-assisted management -description: -tags: - - JupyterHub - - Python - - R ---- - -## Introduction - -Jupyter can execute code in different computing environments using -*kernels*. Some kernels are provided by default (Python, R, etc.) but -you may want to register your computing environment to use it in -notebooks. For example, you may want to load a specific environment -module in your kernel or use a Conda environment. - -To register a Jupyter kernel, you can follow the steps highlighted in -the [Jupyter kernels - Manual management](./Jupyter_kernels_Manual_management.md) -or use the `nesi-add-kernel` tool provided within the [Jupyter on Mahuika service](https://jupyter.nesi.org.nz). -This page details the latter option, which we recommend. - -## Getting started - -First you need to open a terminal. It can be from a session on Jupyter -via OnDemand or from a regular ssh connection on Mahuika login node. If you -use the ssh option, make sure to load the JupyterLab module to have -access to the `nesi-add-kernel` tool: - -``` sh -module purge # remove all previously loaded modules -module load JupyterLab -``` - -Then, to list all available options, use the `-h` or `--help` options as -follows: - -``` sh -nesi-add-kernel --help -``` - -Here is an example to add a TensorFlow kernel, using Mahuika’s module: - -``` sh -nesi-add-kernel tf_kernel TensorFlow/2.8.2-gimkl-2022a-Python-3.10.5 -``` - -!!! warning - The name given to your kernel in `nesi-add-kernel KERNEL_NAME MODULE` must only include lowercase letters, underscores, and dashes. - -and to share the kernel with other members of your project: - -``` sh -nesi-add-kernel --shared tf_kernel_shared TensorFlow/2.8.2-gimkl-2022a-Python-3.10.5 -``` - -To list all the installed kernels, use the following command: - -``` sh -jupyter-kernelspec list -``` - -and to delete a specific kernel: - -``` sh -jupyter-kernelspec remove -``` - -where `` stands for the name of the kernel to delete. - -## Conda environment - -First, make sure the `JupyterLab` module is loaded: - -``` sh -module purge -module load JupyterLab -``` - -To add a Conda environment created using -`conda create -p `, use: - -``` sh -nesi-add-kernel my_conda_env -p -``` - -otherwise if created using `conda create -n `, use: - -``` sh -nesi-add-kernel my_conda_env -n -``` - -## Virtual environment - -If you want to use a Python virtual environment, don’t forget to specify -which Python module you used to create it. - -For example, if we create a virtual environment named `my_test_venv` -using Python 3.10.5: - -``` sh -module purge -module load Python/3.10.5-gimkl-2022a -python -m venv my_test_venv -``` - -to create the corresponding `my_test_kernel` kernel, we need to use the -command: - -``` sh -module purge -module load JupyterLab -nesi-add-kernel my_test_kernel Python/3.10.5-gimkl-2022a --venv my_test_venv -``` - -## Singularity container - -!!! danger - - This section has not been tested on Mahuika OnDemand - -To use a Singularity container, use the `-c` or `--container` options as -follows: - -``` sh -module purge -module load JupyterLab -nesi-add-kernel my_test_kernel -c -``` - -where `` is a path to your container image. - -Note that your container **must** have the `ipykernel` Python package -installed in it to be able to work as a Jupyter kernel. - -Additionally, you can use the `--container-args` option to pass more -arguments to the `singularity exec` command used to instantiate the -kernel. - -Here is an example instantiating a NVIDIA NGC container as a kernel. -First, we need to pull the container: - -``` sh -module purge -module load Singularity/3.11.3 -singularity pull nvidia_tf.sif docker://nvcr.io/nvidia/tensorflow:21.07-tf2-py3 -``` - -then we can instantiate the kernel, using the `--nv` singularity flag to -ensure that the GPU will be found at runtime (assuming our Jupyter -session has access to a GPU): - -``` sh -module purge -module load JupyterLab -nesi-add-kernel nvidia_tf -c nvidia_tf.sif --container-args "'--nv'" -``` - -Note that the double-quoting of `--nv` is needed to properly pass the -options to `singularity exec`. From 2515be1d88a3673d7b8c7225dea4ca8b6ea8f676 Mon Sep 17 00:00:00 2001 From: geoffreyweal Date: Thu, 18 Jun 2026 17:33:46 +1200 Subject: [PATCH 02/10] update --- .../OnDemand/Apps/JupyterLab/.pages.yml | 3 +- .../Apps/JupyterLab/Jupyter_kernels.md | 592 ++++++++++++++++++ .../OnDemand/Apps/JupyterLab/index.md | 9 +- .../Available_Applications/Lambda_Stack.md | 2 +- docs/redirect_map.yml | 6 +- 5 files changed, 603 insertions(+), 9 deletions(-) create mode 100644 docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml index c19a5ee45..9637531df 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml @@ -1,5 +1,4 @@ --- nav: - - Jupyter_kernels_Tool_assisted_management.md - - Jupyter_kernels_Manual_management.md + - Jupyter kernels: Jupyter_kernels.md - "*" diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md new file mode 100644 index 000000000..3bb953772 --- /dev/null +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md @@ -0,0 +1,592 @@ +--- +title: Jupyter kernels +description: How to register and manage custom Jupyter kernels on Mahuika JupyterHub +tags: + - JupyterHub + - Python + - R +--- + +A Jupyter kernel is the actual thing that executes the code in your Jupyter notebook. +Mahuika provides a number of Python and R kernels by default; these can be selected from +the Launcher. These come with many packages preinstalled, and can be extended +further as described on the +[Python](../../../../Software/Available_Applications/Python.md) and +[R](../../../../Software/Available_Applications/R.md) support pages. + +Sometimes, though, you will want a kernel that runs in your own computing +environment - for example, to load a specific environment module, or to use a +Conda or Python virtual environment. There are two ways to register such a +custom kernel; select the tab that suits you: + +- **Tool-Assisted Management**: uses the `nesi-add-kernel` command line tool to + automate most of the setup. This is the **recommended** approach if possible. +- **Manual Management**: you set up the kernel and its wrapper script by hand. This + can be useful when you need more control over how the kernel is launched. + +=== "Tool-Assisted Management" + + The `nesi-add-kernel` tool automates most of the steps needed to register a + kernel. **This is the recommended way to register a Jupyter kernel.** + + First you need to open a terminal. It can be from a session on Jupyter + via OnDemand or from a regular ssh connection on the Mahuika login node. + + - If you use the ssh option, make sure to load the JupyterLab module to have + access to the `nesi-add-kernel` tool: + + ``` sh + module purge # remove all previously loaded modules + module load JupyterLab + ``` + + Use the `nesi-add-kernel` to register a kernel: + + ``` sh + nesi-add-kernel + ``` + + Where: + + - ``: The name you want to give the kernel. + - ``: The environment module to base the kernel on. + + The exact arguments depend on whether you are basing the kernel on an + environment module, a Python virtual environment, or a Conda environment: + + === "Environment module" + + Here is an example for adding a TensorFlow module to JupyterLab as a kernel: + + ``` sh + nesi-add-kernel tf_kernel TensorFlow/2.8.2-gimkl-2022a-Python-3.10.5 + ``` + + === "Python virtual environment" + + To create a Python virtual environment-based kernel, make sure you use + the desired version of python available from `module avail python`. For + example, if we want to use Python 3.10.5, we need to invoke + `Python/3.10.5-gimkl-2022a`, which is what is available when we run + `module avail python`. + + ``` sh + module purge + module load JupyterLab + nesi-add-kernel my_test_kernel Python/3.10.5-gimkl-2022a --venv my_test_venv + ``` + + === "Conda environment" + + Create your [conda environment](../../../../Software/Available_Applications/Miniforge3.md#module-loading-and-conda-environments-isolation): + + ``` sh + module purge && module load Miniforge3 + source $(conda info --base)/etc/profile.d/conda.sh + export PYTHONNOUSERSITE=1 + ``` + + Then create a kernel based on your newly created conda environment: + + ``` sh + nesi-add-kernel my_conda_env -p + ``` + + otherwise if created using `conda create -n `, use: + + ``` sh + nesi-add-kernel my_conda_env -n + ``` + + !!! tip + + For more information about `nesi-add-kernel`, type into the terminal: + + ``` sh + nesi-add-kernel --help + ``` + +=== "Manual Management" + + If you need more control than the `nesi-add-kernel` tool provides, you can + set up a custom kernel by hand. + + You can configure custom kernels for running your Jupyter + notebooks. This could be necessary and/or recommended in some + situations, including: + + - If you wish to load a different combination of environment modules + than those we load in our default kernels + - If you would like to activate a virtual environment or conda + environment before launching the kernel + + The following examples create a custom kernel based on a conda environment, + a Python virtual environment, or an R environment module: + + === "Conda environment" + + First, change directory into the path where you would like to place your + conda environment. + + - If you would like to share it with other members of your project, use + your project folder (`cd /nesi/project/`) + - Avoid paths that include `00_nesi_projects` or `home`, as these cause + issues. + + Second, in a terminal run the following commands to load a + [Miniforge environment module](../../../../Software/Available_Applications/Miniforge3.md#module-loading-and-conda-environments-isolation): + + ``` sh + module purge && module load Miniforge3 + source $(conda info --base)/etc/profile.d/conda.sh + export PYTHONNOUSERSITE=1 + ``` + + Now create a conda environment named `my-conda-env` using Python 3.11. + The *ipykernel* Python package is required but you can change the names + of the environment, version of Python and install other Python packages + as required. + + ``` sh + conda create --prefix ./my-conda-env python=3.11 + conda activate ./my-conda-env + conda install ipykernel + # you can pip/conda install other packages here too + ``` + + Third, we will create a wrapper for your conda environment. + Change directory into your `my-conda-env` folder: + + ``` sh + cd my-conda-env + ``` + + And add the following as `wrapper.sh` into your `my-conda-env` folder + (replace `` with the absolute path to the + environment you created): + + ``` sh + #!/usr/bin/env bash + + # load required modules here + module purge + module load Miniforge3 + + # activate conda environment + source $(conda info --base)/etc/profile.d/conda.sh + conda deactivate # workaround for https://github.com/conda/conda/issues/9392 + conda activate /my-conda-env + + # run the kernel + exec python $@ + ``` + + Make the wrapper script executable: + + ``` sh + chmod +x wrapper.sh + ``` + + Fourth, create a Jupyter kernel based on your new conda environment: + + ``` sh + python -m ipykernel install --user --name my-conda-env --display-name="My Conda Env" + ``` + + We must now edit the kernel to load the required environment + modules before the kernel is launched. Change to the directory the + kernelspec was installed to + `~/.local/share/jupyter/kernels/my-conda-env`, (assuming you kept + `--name my-conda-env` in the above command): + + ``` sh + cd ~/.local/share/jupyter/kernels/my-conda-env + ``` + + and edit the *kernel.json* to change the first element of the argv list + to point to the wrapper script we just created. The file should look + like this: + + ```json + { + "argv": [ + "/wrapper.sh", + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}" + ], + "display_name": "My Conda Env", + "language": "python" + } + ``` + + After refreshing JupyterLab your new kernel should show up in the + Launcher as "My Conda Env". + + === "Python virtual environment" + + First, change directory into the path where you would like to place your + virtual environment. + + - If you would like to share it with other members of your project, use + your project folder (`cd /nesi/project/`) + - avoid paths that include `00_nesi_projects` or `home`, as these cause + issues. + + Second, in a terminal run the following commands to load a Python + environment module: + + ``` sh + module purge + module load Python/3.14.4-foss-2026 + ``` + + Now create a Python virtual environment named `my-venv`. + The *ipykernel* Python package is required but you can change the name + of the environment and install other Python packages as required. + + ``` sh + python3 -m venv ./my-venv + source ./my-venv/bin/activate + pip install --upgrade pip + pip install ipykernel + # you can pip install other packages here too + ``` + + Third, we will create a wrapper for your virtual environment. + Change directory into your `my-venv` folder: + + ``` sh + cd my-venv + ``` + + And add the following as `wrapper.sh` into your `my-venv` folder: + + ``` sh + #!/usr/bin/env bash + + # load required modules here + module purge + module load Python/3.14.4-foss-2026 + + # activate virtual environment + source /my-venv/bin/activate + + # run the kernel + exec python $@ + ``` + + Make the wrapper script executable: + + ``` sh + chmod +x wrapper.sh + ``` + + Fourth, create a Jupyter kernel based on your new virtual environment: + + ``` sh + python -m ipykernel install --user --name my-venv --display-name="My Venv" + ``` + + We must now edit the kernel to load the required environment + modules before the kernel is launched. Change to the directory the + kernelspec was installed to + `~/.local/share/jupyter/kernels/my-venv`, (assuming you kept + `--name my-venv` in the above command): + + ``` sh + mkdir -p ~/.local/share/jupyter/kernels/my-venv + cd ~/.local/share/jupyter/kernels/my-venv + ``` + + and edit the *kernel.json* to change the first element of the argv list + to point to the wrapper script we just created. The file should look + like this: + + ```json + { + "argv": [ + "/wrapper.sh", + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}" + ], + "display_name": "My Venv", + "language": "python" + } + ``` + + After refreshing JupyterLab your new kernel should show up in the + Launcher as "My Venv". + + === "R kernel" + + You can configure custom R kernels for running your Jupyter notebooks. + The following example will create a custom kernel based on the + R/3.6.2-gimkl-2020a environment module and will additionally load an + MPFR environment module (e.g. if you wanted to load the Rmpfr package). + + In a terminal run the following commands to load the required + environment modules: + + ``` sh + module purge + module load IRkernel/1.1.1-gimkl-2020a-R-3.6.2 + module load Python/3.8.2-gimkl-2020a + ``` + + The IRkernel module loads the R module as a dependency and provides the + R kernel for Jupyter. Python is required to install the kernel (since + Jupyter is written in Python). + + Now create an R Jupyter kernel: + + ``` sh + R -e "IRkernel::installspec(name='myrwithmpfr', displayname = 'R with MPFR', user = TRUE)" + ``` + + We must now edit the kernel to load the required environment + modules when the kernel is launched. Change to the directory the + kernelspec was installed to + (~/.local/share/jupyter/kernels/myrwithmpfr, assuming you kept `--name + myrwithmpfr` in the above command): + + ``` sh + cd ~/.local/share/jupyter/kernels/myrwithmpfr + ``` + + Now create a wrapper script in that directory, called *wrapper.sh*, with + the following contents: + + ``` sh + #!/usr/bin/env bash + + # load required modules here + module purge + module load MPFR/4.0.2-GCCcore-9.2.0 + module load IRkernel/1.1.1-gimkl-2020a-R-3.6.2 + + # run the kernel + exec R $@ + ``` + + Make the wrapper script executable: + + ``` sh + chmod +x wrapper.sh + ``` + + and edit the *kernel.json* to change the first element of the argv list + to point to the wrapper script we just created. The file should look + like this: + + ```json + { + "argv": [ + "/home//.local/share/jupyter/kernels/myrwithmpfr/wrapper.sh", + "--slave", + "-e", + "IRkernel::main()", + "--args", + "{connection_file}" + ], + "display_name": "R with MPFR", + "language": "R" + } + ``` + + After refreshing JupyterLab your new R kernel should show up in the + Launcher as "R with MPFR". + +## Listing your kernels + +To list all the kernels that are currently installed, run: + +``` sh +jupyter-kernelspec list +``` + +This works for any kernel, regardless of whether it was registered using the +Tool-Assisted Management or Manual Management approach. + +## Sharing a kernel + +You can also configure a shared kernel that others with access to the same +project will be able to load. How you do this depends on whether you registered +the kernel with the tool-assisted or manual approach: + +=== "Tool-Assisted Management" + + When registering the kernel with `nesi-add-kernel`, add the `--shared` flag + to make it available to other members of your project. Select the tab for + the kind of kernel you are sharing: + + === "Environment module" + + ``` sh + nesi-add-kernel --shared tf_kernel_shared TensorFlow/2.8.2-gimkl-2022a-Python-3.10.5 + ``` + + === "Python virtual environment" + + ``` sh + nesi-add-kernel --shared my_test_kernel Python/3.10.5-gimkl-2022a --venv my_test_venv + ``` + + === "Conda environment" + + Create your [conda environment](../../../../Software/Available_Applications/Miniforge3.md#module-loading-and-conda-environments-isolation): + + ``` sh + module purge && module load Miniforge3 + source $(conda info --base)/etc/profile.d/conda.sh + export PYTHONNOUSERSITE=1 + ``` + + Then create a kernel based on your newly created conda environment. + Make sure you include the `--shared` flag when you run `nesi-add-kernel`: + + ``` sh + nesi-add-kernel --shared my_conda_env -p + ``` + + otherwise if created using `conda create -n `, use: + + ``` sh + nesi-add-kernel --shared my_conda_env -n + ``` + +=== "Manual Management" + + For a manually-created kernel, you must make sure it also exists in a shared + location (other users cannot see your home directory). + + First, **you** need to create the kernel yourself, following the manual steps + in the **Manual Management** tab earlier on this page. + + Next, **your team members** need to set up the kernel on their side. The + exact steps depend on the type of environment you created, so follow the + steps in the tab that matches it: + + === "Conda environment" + + Second, **your team members** need to run the following commands in the + terminal: + + ``` sh + # change directory into the path that contains your conda environment + cd + + # load Miniforge3 + module purge + module load Miniforge3 + + # Activate your shared conda environment + source $(conda info --base)/etc/profile.d/conda.sh + conda activate ./my-conda-env + ``` + + Third, get **your team members** to create a Jupyter kernel based on your + conda environment: + + ``` sh + python -m ipykernel install --user --name my-conda-env --display-name="My Conda Env" + ``` + + **Your project members** must now edit the kernel in their home directories + to load the required environment modules before the kernel is launched. + Change to the directory the kernelspec was installed to + `~/.local/share/jupyter/kernels/my-conda-env`, (assuming you kept + `--name my-conda-env` in the above command): + + ``` sh + mkdir -p ~/.local/share/jupyter/kernels/my-conda-env + cd ~/.local/share/jupyter/kernels/my-conda-env + ``` + + and edit the *kernel.json* to change the first element of the argv list + to point to the wrapper script we just created. The file should look + like this: + + ```json + { + "argv": [ + "/wrapper.sh", + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}" + ], + "display_name": "My Conda Env", + "language": "python" + } + ``` + + After refreshing JupyterLab your new kernel should show up in the + Launcher as "My Conda Env". + + === "Python virtual environment" + + Second, **your team members** need to run the following commands in the + terminal: + + ``` sh + # load the Python environment module + module purge + module load Python/3.14.4-foss-2026 + + # Activate the shared virtual environment + source /my-venv/bin/activate + ``` + + Third, get **your team members** to create a Jupyter kernel based on your + virtual environment: + + ``` sh + python -m ipykernel install --user --name my-venv --display-name="My Venv" + ``` + + **Your project members** must now edit the kernel in their home directories + to load the required environment modules before the kernel is launched. + Change to the directory the kernelspec was installed to + `~/.local/share/jupyter/kernels/my-venv`, (assuming you kept + `--name my-venv` in the above command): + + ``` sh + mkdir -p ~/.local/share/jupyter/kernels/my-venv + cd ~/.local/share/jupyter/kernels/my-venv + ``` + + and edit the *kernel.json* to change the first element of the argv list + to point to the wrapper script we just created. The file should look + like this: + + ```json + { + "argv": [ + "/wrapper.sh", + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}" + ], + "display_name": "My Venv", + "language": "python" + } + ``` + + After refreshing JupyterLab your new kernel should show up in the + Launcher as "My Venv". + +## Removing a kernel + +To delete a specific kernel, run: + +``` sh +jupyter-kernelspec remove +``` + +where `` is the name of the kernel to delete, as shown by +`jupyter-kernelspec list`. diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/index.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/index.md index 80d2b9e1f..d61e83c23 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/index.md +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/index.md @@ -38,8 +38,8 @@ Mahuika provides some default Python and R kernels that are available to all use of environment modules. It's also possible to create additional kernels that are visible only to you (they can optionally be made visible to other members of a specific project that you belong to). See: -- [Jupyter kernels - Tool-assisted management](./Jupyter_kernels_Tool_assisted_management.md) (recommended) -- [Jupyter kernels - Manual management](./Jupyter_kernels_Manual_management.md) +- [Jupyter kernels - Tool-Assisted Management](./Jupyter_kernels.md) (recommended) +- [Jupyter kernels - Manual Management](./Jupyter_kernels.md) ### Jupyter terminal @@ -105,8 +105,9 @@ If your JupyterLab notebook "jumps" or skips pages unexpectedly, the cause is li #### The Solution: Adjust Windowing Mode To fix erratic scrolling, change the Windowing Mode setting to one of the following: -* `defer`: Waits for idle CPU cycles to render cells outside the viewport. This typically stops the jumping while maintaining good performance. -* `none`: Renders all cells immediately. This is the most stable option for scrolling but may slow down very large notebooks. + +- `defer`: Waits for idle CPU cycles to render cells outside the viewport. This typically stops the jumping while maintaining good performance. +- `none`: Renders all cells immediately. This is the most stable option for scrolling but may slow down very large notebooks. #### Step-by-Step Instructions 1. Open Settings: Go to Settings → Settings Editor in the top menu. diff --git a/docs/Software/Available_Applications/Lambda_Stack.md b/docs/Software/Available_Applications/Lambda_Stack.md index 65db375bc..ce93d39d1 100644 --- a/docs/Software/Available_Applications/Lambda_Stack.md +++ b/docs/Software/Available_Applications/Lambda_Stack.md @@ -70,7 +70,7 @@ ${CONTAINER} echo "Hello World" The following steps will create a custom Lambda Stack kernel that can be accessed via Mahuika's Jupyter service (based on the instructions at -[Jupyter_on_Mahuika](../../Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md)). +[Jupyter_on_Mahuika](../../Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md)). First, we need to create a kernel definition and wrapper that will launch the container image. Run the following commands on the Mahuika diff --git a/docs/redirect_map.yml b/docs/redirect_map.yml index e9c845e1b..5367e5c18 100644 --- a/docs/redirect_map.yml +++ b/docs/redirect_map.yml @@ -80,8 +80,10 @@ Scientific_Computing/Interactive_computing_with_OnDemand/Apps/RStudio.md : Inter Scientific_Computing/Interactive_computing_with_OnDemand/Apps/virtual_desktop.md : Interactive_Computing/OnDemand/Apps/virtual_desktop.md Scientific_Computing/Interactive_computing_with_OnDemand/Apps/VSCode.md : Interactive_Computing/OnDemand/Apps/VSCode.md Scientific_Computing/Interactive_computing_with_OnDemand/Apps/JupyterLab/index.md : Interactive_Computing/OnDemand/Apps/JupyterLab/index.md -Scientific_Computing/Interactive_computing_with_OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md -Scientific_Computing/Interactive_computing_with_OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md +Scientific_Computing/Interactive_computing_with_OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md +Scientific_Computing/Interactive_computing_with_OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md +Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md +Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md Scientific_Computing/Interactive_computing_with_OnDemand/Release_Notes/index.md : Interactive_Computing/OnDemand/Release_Notes/index.md General/Announcements/Accessing_NeSI_Support_during_the_Easter_break.md : Announcements/Accessing_NeSI_Support_during_the_Easter_break.md General/Announcements/Autodeletion_of_Scratch_Filesystem.md : Announcements/Autodeletion_of_Scratch_Filesystem.md From 82d727cf3ffbea3f4d7478b0051b338cea14a818 Mon Sep 17 00:00:00 2001 From: geoffreyweal Date: Fri, 19 Jun 2026 12:14:30 +1200 Subject: [PATCH 03/10] update --- .../Apps/JupyterLab/Jupyter_kernels.md | 172 ++++++++++++++---- 1 file changed, 133 insertions(+), 39 deletions(-) diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md index 3bb953772..ee5b077e1 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md @@ -29,32 +29,32 @@ custom kernel; select the tab that suits you: The `nesi-add-kernel` tool automates most of the steps needed to register a kernel. **This is the recommended way to register a Jupyter kernel.** - First you need to open a terminal. It can be from a session on Jupyter - via OnDemand or from a regular ssh connection on the Mahuika login node. + The exact arguments depend on whether you are basing the kernel on an + environment module, a Python virtual environment, or a Conda environment: - - If you use the ssh option, make sure to load the JupyterLab module to have - access to the `nesi-add-kernel` tool: + === "Environment module" - ``` sh - module purge # remove all previously loaded modules - module load JupyterLab - ``` + First you need to open a terminal. It can be from a session on Jupyter + via OnDemand or from a regular ssh connection on the Mahuika login node. - Use the `nesi-add-kernel` to register a kernel: + - If you use the ssh option, make sure to load the JupyterLab module to + have access to the `nesi-add-kernel` tool: - ``` sh - nesi-add-kernel - ``` + ``` sh + module purge # remove all previously loaded modules + module load JupyterLab + ``` - Where: + Use the `nesi-add-kernel` to register a kernel: - - ``: The name you want to give the kernel. - - ``: The environment module to base the kernel on. + ``` sh + nesi-add-kernel + ``` - The exact arguments depend on whether you are basing the kernel on an - environment module, a Python virtual environment, or a Conda environment: + Where: - === "Environment module" + - ``: The name you want to give the kernel. + - ``: The environment module to base the kernel on. Here is an example for adding a TensorFlow module to JupyterLab as a kernel: @@ -64,23 +64,33 @@ custom kernel; select the tab that suits you: === "Python virtual environment" - To create a Python virtual environment-based kernel, make sure you use - the desired version of python available from `module avail python`. For - example, if we want to use Python 3.10.5, we need to invoke - `Python/3.10.5-gimkl-2022a`, which is what is available when we run - `module avail python`. + First, create your [python virtual environment](../../../../Software/Available_Applications/Python.md#installing-packages-in-your-home): + + ``` sh + module purge + module load Python/3.14.4-foss-2026 # Change the module to the version of python you want to use + python3 -m venv ./my-venv + pip install --upgrade pip + # you can pip install other packages here too + ``` + + Then create a kernel based on your virtual environment: ``` sh module purge module load JupyterLab - nesi-add-kernel my_test_kernel Python/3.10.5-gimkl-2022a --venv my_test_venv + # The module of python you give here must be the same as the version of python you use to make the virtual environment + nesi-add-kernel --venv my-venv ``` + Where `` is the name you want to give to the kernel. + === "Conda environment" - Create your [conda environment](../../../../Software/Available_Applications/Miniforge3.md#module-loading-and-conda-environments-isolation): + First, create your [conda environment](../../../../Software/Available_Applications/Miniforge3.md#module-loading-and-conda-environments-isolation). You will need to begin by loading conda: ``` sh + # Load conda module purge && module load Miniforge3 source $(conda info --base)/etc/profile.d/conda.sh export PYTHONNOUSERSITE=1 @@ -89,15 +99,32 @@ custom kernel; select the tab that suits you: Then create a kernel based on your newly created conda environment: ``` sh - nesi-add-kernel my_conda_env -p + # Load JupyterLab + module load JupyterLab + + # Create your conda environment + conda create --prefix /my_conda_env python=3.11 + + # Add your conda environment as a kernel to JupyterHub + nesi-add-kernel -p ``` - otherwise if created using `conda create -n `, use: + Where `` is the name for your kernel. Alternatively, you can + create the environment by name: ``` sh - nesi-add-kernel my_conda_env -n + # Load JupyterLab + module load JupyterLab + + # Create your conda environment + conda create -n + + # Add your conda environment as a kernel to JupyterHub + nesi-add-kernel -n ``` + Where `` is the name for your kernel. + !!! tip For more information about `nesi-add-kernel`, type into the terminal: @@ -405,6 +432,8 @@ custom kernel; select the tab that suits you: To list all the kernels that are currently installed, run: ``` sh +module purge +module load JupyterLab jupyter-kernelspec list ``` @@ -419,45 +448,108 @@ the kernel with the tool-assisted or manual approach: === "Tool-Assisted Management" - When registering the kernel with `nesi-add-kernel`, add the `--shared` flag - to make it available to other members of your project. Select the tab for - the kind of kernel you are sharing: + To share a kernel, register it with `nesi-add-kernel` as you normally would, + but add the `--shared` flag so other members of your project can load it. + Select the tab for the kind of kernel you are sharing: === "Environment module" + First you need to open a terminal. It can be from a session on Jupyter + via OnDemand or from a regular ssh connection on the Mahuika login node. + + - If you use the ssh option, make sure to load the JupyterLab module to + have access to the `nesi-add-kernel` tool: + + ``` sh + module purge # remove all previously loaded modules + module load JupyterLab + ``` + + Use the `nesi-add-kernel` command with the `--shared` flag to register a + shared kernel: + + ``` sh + nesi-add-kernel --shared + ``` + + Where: + + - ``: The name you want to give the kernel. + - ``: The environment module to base the kernel on. + + Here is an example for adding a shared TensorFlow module to JupyterLab as + a kernel: + ``` sh nesi-add-kernel --shared tf_kernel_shared TensorFlow/2.8.2-gimkl-2022a-Python-3.10.5 ``` === "Python virtual environment" + First, create your [python virtual environment](../../../../Software/Available_Applications/Python.md#installing-packages-in-your-home): + + ``` sh + module purge + module load Python/3.14.4-foss-2026 # Change the module to the version of python you want to use + python3 -m venv ./my-venv + pip install --upgrade pip + # you can pip install other packages here too + ``` + + Then create a shared kernel based on your virtual environment: + ``` sh - nesi-add-kernel --shared my_test_kernel Python/3.10.5-gimkl-2022a --venv my_test_venv + module purge + module load JupyterLab + # The module of python you give here must be the same as the version of python you use to make the virtual environment + nesi-add-kernel --shared --venv my-venv ``` + Where `` is the name you want to give to the kernel. Note + the `--shared` flag in the `nesi-add-kernel` command line. + === "Conda environment" - Create your [conda environment](../../../../Software/Available_Applications/Miniforge3.md#module-loading-and-conda-environments-isolation): + First, create your [conda environment](../../../../Software/Available_Applications/Miniforge3.md#module-loading-and-conda-environments-isolation). You will need to begin by loading conda: ``` sh + # Load conda module purge && module load Miniforge3 source $(conda info --base)/etc/profile.d/conda.sh export PYTHONNOUSERSITE=1 ``` - Then create a kernel based on your newly created conda environment. - Make sure you include the `--shared` flag when you run `nesi-add-kernel`: + Then create a shared kernel based on your newly created conda environment: ``` sh - nesi-add-kernel --shared my_conda_env -p + # Load JupyterLab + module load JupyterLab + + # Create your conda environment + conda create --prefix /my_conda_env python=3.11 + + # Add your conda environment as a shared kernel to JupyterHub + nesi-add-kernel --shared -p ``` - otherwise if created using `conda create -n `, use: + Where `` is the name you want to give to the kernel. Note + the `--shared` flag in the `nesi-add-kernel` command line. Alternatively, + you can create the environment by name: ``` sh - nesi-add-kernel --shared my_conda_env -n + # Load JupyterLab + module load JupyterLab + + # Create your conda environment + conda create -n + + # Add your conda environment as a shared kernel to JupyterHub + nesi-add-kernel --shared -n ``` + Where `` is the name you want to give to the kernel. Note + the `--shared` flag in the `nesi-add-kernel` command line. + === "Manual Management" For a manually-created kernel, you must make sure it also exists in a shared @@ -502,7 +594,6 @@ the kernel with the tool-assisted or manual approach: `--name my-conda-env` in the above command): ``` sh - mkdir -p ~/.local/share/jupyter/kernels/my-conda-env cd ~/.local/share/jupyter/kernels/my-conda-env ``` @@ -585,6 +676,9 @@ the kernel with the tool-assisted or manual approach: To delete a specific kernel, run: ``` sh +module purge +module load JupyterLab +jupyter-kernelspec list jupyter-kernelspec remove ``` From 101b71910d34bb68f55c9c9d8ffab4ed24bcfca5 Mon Sep 17 00:00:00 2001 From: geoffreyweal Date: Fri, 19 Jun 2026 12:23:03 +1200 Subject: [PATCH 04/10] update title --- .../{Jupyter_kernels.md => python_and_r_kernels_in_JupyterLab.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/Interactive_Computing/OnDemand/Apps/JupyterLab/{Jupyter_kernels.md => python_and_r_kernels_in_JupyterLab.md} (100%) diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md similarity index 100% rename from docs/Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md rename to docs/Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md From c9221c4a768b48f2b2fbb986c890213420449289 Mon Sep 17 00:00:00 2001 From: geoffreyweal Date: Fri, 19 Jun 2026 12:23:10 +1200 Subject: [PATCH 05/10] update title --- .../OnDemand/Apps/JupyterLab/.pages.yml | 2 +- .../OnDemand/Apps/JupyterLab/index.md | 4 ++-- .../JupyterLab/python_and_r_kernels_in_JupyterLab.md | 4 +++- docs/Software/Available_Applications/Lambda_Stack.md | 2 +- docs/redirect_map.yml | 9 +++++---- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml index 9637531df..644e4fd74 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml @@ -1,4 +1,4 @@ --- nav: - - Jupyter kernels: Jupyter_kernels.md + - Python and R kernels in JupyterLab: python_and_r_kernels_in_JupyterLab.md - "*" diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/index.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/index.md index d61e83c23..239136e32 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/index.md +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/index.md @@ -38,8 +38,8 @@ Mahuika provides some default Python and R kernels that are available to all use of environment modules. It's also possible to create additional kernels that are visible only to you (they can optionally be made visible to other members of a specific project that you belong to). See: -- [Jupyter kernels - Tool-Assisted Management](./Jupyter_kernels.md) (recommended) -- [Jupyter kernels - Manual Management](./Jupyter_kernels.md) +- [Python and R kernels in JupyterLab - Tool-Assisted Management](./python_and_r_kernels_in_JupyterLab.md) (recommended) +- [Python and R kernels in JupyterLab - Manual Management](./python_and_r_kernels_in_JupyterLab.md) ### Jupyter terminal diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md index ee5b077e1..545fce5b4 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md @@ -1,5 +1,5 @@ --- -title: Jupyter kernels +title: Python and R kernels in JupyterLab description: How to register and manage custom Jupyter kernels on Mahuika JupyterHub tags: - JupyterHub @@ -14,6 +14,8 @@ further as described on the [Python](../../../../Software/Available_Applications/Python.md) and [R](../../../../Software/Available_Applications/R.md) support pages. +## Adding a Kernel to JupyterLab + Sometimes, though, you will want a kernel that runs in your own computing environment - for example, to load a specific environment module, or to use a Conda or Python virtual environment. There are two ways to register such a diff --git a/docs/Software/Available_Applications/Lambda_Stack.md b/docs/Software/Available_Applications/Lambda_Stack.md index ce93d39d1..5d87bed85 100644 --- a/docs/Software/Available_Applications/Lambda_Stack.md +++ b/docs/Software/Available_Applications/Lambda_Stack.md @@ -70,7 +70,7 @@ ${CONTAINER} echo "Hello World" The following steps will create a custom Lambda Stack kernel that can be accessed via Mahuika's Jupyter service (based on the instructions at -[Jupyter_on_Mahuika](../../Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md)). +[Jupyter_on_Mahuika](../../Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md)). First, we need to create a kernel definition and wrapper that will launch the container image. Run the following commands on the Mahuika diff --git a/docs/redirect_map.yml b/docs/redirect_map.yml index 5367e5c18..c121e5485 100644 --- a/docs/redirect_map.yml +++ b/docs/redirect_map.yml @@ -80,10 +80,11 @@ Scientific_Computing/Interactive_computing_with_OnDemand/Apps/RStudio.md : Inter Scientific_Computing/Interactive_computing_with_OnDemand/Apps/virtual_desktop.md : Interactive_Computing/OnDemand/Apps/virtual_desktop.md Scientific_Computing/Interactive_computing_with_OnDemand/Apps/VSCode.md : Interactive_Computing/OnDemand/Apps/VSCode.md Scientific_Computing/Interactive_computing_with_OnDemand/Apps/JupyterLab/index.md : Interactive_Computing/OnDemand/Apps/JupyterLab/index.md -Scientific_Computing/Interactive_computing_with_OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md -Scientific_Computing/Interactive_computing_with_OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md -Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md -Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md +Scientific_Computing/Interactive_computing_with_OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md +Scientific_Computing/Interactive_computing_with_OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md +Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Manual_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md +Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels_Tool_assisted_management.md : Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md +Interactive_Computing/OnDemand/Apps/JupyterLab/Jupyter_kernels.md : Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md Scientific_Computing/Interactive_computing_with_OnDemand/Release_Notes/index.md : Interactive_Computing/OnDemand/Release_Notes/index.md General/Announcements/Accessing_NeSI_Support_during_the_Easter_break.md : Announcements/Accessing_NeSI_Support_during_the_Easter_break.md General/Announcements/Autodeletion_of_Scratch_Filesystem.md : Announcements/Autodeletion_of_Scratch_Filesystem.md From f08c427641064e1520011b1aeddeafbc674d707b Mon Sep 17 00:00:00 2001 From: geoffreyweal Date: Fri, 19 Jun 2026 13:43:57 +1200 Subject: [PATCH 06/10] update for using a container as a kernel in Jupyterlab --- .../OnDemand/Apps/JupyterLab/.pages.yml | 1 + .../containers_as_kernels_in_JupyterLab.md | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml index 644e4fd74..0e6dbd1b9 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/.pages.yml @@ -1,4 +1,5 @@ --- nav: - Python and R kernels in JupyterLab: python_and_r_kernels_in_JupyterLab.md + - Containers as kernels in JupyterLab: containers_as_kernels_in_JupyterLab.md - "*" diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md new file mode 100644 index 000000000..694999f11 --- /dev/null +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md @@ -0,0 +1,120 @@ +--- +title: Containers as kernels in JupyterLab +description: How to use containers as kernels in JupyterLab on Mahuika +tags: + - JupyterHub +--- + +To run a container as a kernel in JupyterLab, you first need to set up a Python +virtual environment by hand, in the same way as the Manual Management +approach described on the +[Python and R kernels in JupyterLab](./python_and_r_kernels_in_JupyterLab.md) +page. + +First, change directory into the path where you would like to place your +virtual environment. + +- If you would like to share it with other members of your project, use your + project folder (`cd /nesi/project/`) +- avoid paths that include `00_nesi_projects` or `home`, as these cause issues. + +Second, in a terminal run the following commands to load a Python environment +module: + +``` sh +module purge +module load Python/3.14.4-foss-2026 +``` + +Now create a Python virtual environment named `my-container-venv`. You can +change the name of the environment and install other Python packages as +required. + +``` sh +python3 -m venv ./my-container-venv +source ./my-container-venv/bin/activate +pip install --upgrade pip +``` + +Third, you will want to install `bash_kernel` in your virtual environment. + +??? note "What is bash_kernel?" + + `bash_kernel` allows you to use python as a terminal. We will use `bash_kernel` to connect python to your container, allowing your experience of using the container to feel like a normal jupyterlab session. But instead of running python, it runs your container. + +``` sh +pip install bash_kernel +``` + +Fourth, we will create a wrapper for your virtual environment. Change directory +into your `my-container-venv` folder: + +``` sh +cd my-container-venv +``` + +And add the following as `wrapper.sh` into your `my-container-venv` folder: + +``` sh +#!/usr/bin/env bash + +# load required modules here +module purge +module load Python/3.14.4-foss-2026 + +# tell bash_kernel to run inside your container instead of a normal bash shell +export BASH_KERNEL_CMD="apptainer exec --nv bash" + +# run bash_kernel on the host (it dispatches every command into the container) +exec python3 -m bash_kernel "$@" +``` + +The `BASH_KERNEL_CMD` environment variable is what makes this a *container* +kernel: `bash_kernel` itself runs on the host, but every cell you run in +JupyterLab is executed by the `bash` inside the container you point to. Update +the `apptainer exec` line to match your own image, for example: + +``` sh +export BASH_KERNEL_CMD="apptainer exec --pwd /opt/PMDM --nv /nesi/project//containers/PMDM/pmdm_cu130.simg bash" +``` + +Make the wrapper script executable: + +``` sh +chmod +x wrapper.sh +``` + +Fifth, install the bash kernel based on your new virtual environment: + +``` sh +python -m bash_kernel.install --user +``` + +This installs a kernel into `~/.local/share/jupyter/kernels/bash`. We must now +edit it so the kernel runs through the wrapper script we created above. Change +to the directory the kernelspec was installed to: + +``` sh +cd ~/.local/share/jupyter/kernels/bash +``` + +and edit the *kernel.json* to change the first element of the argv list to point +to the wrapper script we just created. The file should look like this: + +```json +{ + "argv": [ + "/wrapper.sh", + "-m", + "bash_kernel", + "-f", + "{connection_file}" + ], + "display_name": "Container Name", + "language": "bash" +} +``` + +After refreshing JupyterLab your new kernel should show up in the Launcher as +"My Container Venv". + From 5ea167ed7f5b3dadec61df32a0237924020ff7c1 Mon Sep 17 00:00:00 2001 From: geoffreyweal Date: Fri, 19 Jun 2026 14:14:16 +1200 Subject: [PATCH 07/10] Update container page --- .../containers_as_kernels_in_JupyterLab.md | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md index 694999f11..45edd257e 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md @@ -11,12 +11,16 @@ approach described on the [Python and R kernels in JupyterLab](./python_and_r_kernels_in_JupyterLab.md) page. +In brief, the steps below create a Python virtual environment, install +`bash_kernel` into it, write a wrapper script that points `bash_kernel` at your +container, and then register that wrapper as a Jupyter kernel. + First, change directory into the path where you would like to place your virtual environment. - If you would like to share it with other members of your project, use your - project folder (`cd /nesi/project/`) -- avoid paths that include `00_nesi_projects` or `home`, as these cause issues. + project folder (`cd /nesi/project/`). +- Avoid paths that include `00_nesi_projects` or `home`, as these cause issues. Second, in a terminal run the following commands to load a Python environment module: @@ -36,17 +40,17 @@ source ./my-container-venv/bin/activate pip install --upgrade pip ``` -Third, you will want to install `bash_kernel` in your virtual environment. +Third, install `bash_kernel` in your virtual environment. ??? note "What is bash_kernel?" - `bash_kernel` allows you to use python as a terminal. We will use `bash_kernel` to connect python to your container, allowing your experience of using the container to feel like a normal jupyterlab session. But instead of running python, it runs your container. + `bash_kernel` runs a bash shell as a Jupyter kernel. Here we point that bash shell at your container, so every cell you run in JupyterLab executes inside the container. It feels like a normal JupyterLab session, but the commands run in your container rather than on the host. ``` sh pip install bash_kernel ``` -Fourth, we will create a wrapper for your virtual environment. Change directory +Fourth, create a wrapper for your virtual environment. Change directory into your `my-container-venv` folder: ``` sh @@ -62,11 +66,14 @@ And add the following as `wrapper.sh` into your `my-container-venv` folder: module purge module load Python/3.14.4-foss-2026 +# activate the virtual environment that has bash_kernel installed +source /my-container-venv/bin/activate + # tell bash_kernel to run inside your container instead of a normal bash shell export BASH_KERNEL_CMD="apptainer exec --nv bash" -# run bash_kernel on the host (it dispatches every command into the container) -exec python3 -m bash_kernel "$@" +# run the kernel on the host (it dispatches every command into the container) +exec python3 "$@" ``` The `BASH_KERNEL_CMD` environment variable is what makes this a *container* @@ -84,10 +91,10 @@ Make the wrapper script executable: chmod +x wrapper.sh ``` -Fifth, install the bash kernel based on your new virtual environment: +Fifth, install `bash_kernel` based on your new virtual environment: ``` sh -python -m bash_kernel.install --user +python3 -m bash_kernel.install --user ``` This installs a kernel into `~/.local/share/jupyter/kernels/bash`. We must now @@ -115,6 +122,7 @@ to the wrapper script we just created. The file should look like this: } ``` -After refreshing JupyterLab your new kernel should show up in the Launcher as -"My Container Venv". +After refreshing JupyterLab your new kernel should show up in the Launcher under +the display name you set in `kernel.json` (`Container Name` in the example +above). From 0a70d797b5ca505d9d3c5c194404dfdc3c73eee1 Mon Sep 17 00:00:00 2001 From: geoffreyweal Date: Sun, 21 Jun 2026 12:17:57 +1200 Subject: [PATCH 08/10] Update to include kernel that is a version of python inside a container --- .../containers_as_kernels_in_JupyterLab.md | 477 ++++++++++++++---- .../python_and_r_kernels_in_JupyterLab.md | 94 ++-- 2 files changed, 429 insertions(+), 142 deletions(-) diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md index 45edd257e..2cf9bede2 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md @@ -5,124 +5,425 @@ tags: - JupyterHub --- -To run a container as a kernel in JupyterLab, you first need to set up a Python -virtual environment by hand, in the same way as the Manual Management -approach described on the -[Python and R kernels in JupyterLab](./python_and_r_kernels_in_JupyterLab.md) -page. +Apptainer containers can be run as kernels in JupyterLab, so that the code in +your notebook executes inside the container. -In brief, the steps below create a Python virtual environment, install -`bash_kernel` into it, write a wrapper script that points `bash_kernel` at your -container, and then register that wrapper as a Jupyter kernel. +## Adding a container as a kernel to JupyterLab -First, change directory into the path where you would like to place your -virtual environment. +There are two ways to run a container as a kernel; select the tab that suits you: -- If you would like to share it with other members of your project, use your - project folder (`cd /nesi/project/`). -- Avoid paths that include `00_nesi_projects` or `home`, as these cause issues. +- **Tool-Assisted Management**: uses the `nesi-add-kernel` command line tool to + automate most of the setup. This is the **recommended** approach if possible. +- **Manual Management**: you set up the kernel and its wrapper script by hand. + This can be useful when you need more control over how the kernel is launched. -Second, in a terminal run the following commands to load a Python environment -module: +=== "Tool-Assisted Management" + + The `nesi-add-kernel` tool can register a container as a kernel for you. + **This is the recommended way to run a container as a kernel.** + + First you need to open a terminal. It can be from a session on Jupyter via + OnDemand or from a regular ssh connection on the Mahuika login node. + + - If you use the ssh option, make sure to load the JupyterLab module to have + access to the `nesi-add-kernel` tool: + + ``` sh + module purge # remove all previously loaded modules + module load JupyterLab + ``` + + There are two ways to run a container as a kernel; select the tab that suits + your container: + + === "Python kernel" + + This runs a **Python** kernel *inside* the container. The container must + provide a Python interpreter with the `ipykernel` package installed. + + ``` sh + nesi-add-kernel -cp + ``` + + Where: + + - ``: the name you want to give the kernel. + - ``: the path to your Apptainer container image. + + === "Bash kernel" + + This runs a **bash** kernel that dispatches every cell into the + container. + + ``` sh + nesi-add-kernel -cb + ``` + + Where: + + - ``: the name you want to give the kernel. + - ``: the path to your Apptainer container image. + + !!! tip "Passing arguments to Apptainer" + + Use the `--container-args` option to pass additional arguments through to + the `apptainer exec` command (this works for both `-cp` and `-cb`). + Because these arguments start with `-`, use the `=` form so they are not + mistaken for options to `nesi-add-kernel` itself. For example, to enable + GPU support: + + ``` sh + nesi-add-kernel -cp --container-args="--nv" + ``` + + Multiple arguments can be passed together: + + ``` sh + nesi-add-kernel -cp --container-args="--nv --pwd /opt/app" + ``` + + !!! tip + + For more information about `nesi-add-kernel`, type into the terminal: + + ``` sh + nesi-add-kernel --help + ``` + +=== "Manual Management" + + You can also set up a container kernel by hand. You can build a kernel that + uses a container's version of python or bash: + + === "Python kernel" + + This runs a **Python** kernel *inside* the container (the manual + equivalent of the `-cp` option), so the container must provide a Python + interpreter with the `ipykernel` package installed. + + First, change directory into the path where you would like to place your + wrapper script. + + - If you would like to share it with other members of your project, use + your project folder (`cd /nesi/project/`). + - Avoid paths that include `00_nesi_projects` or `home`, as these cause + issues. + + Second, create a file called `wrapper.sh` with the following contents + (replace `` with the path to your container + image): + + ``` sh + #!/usr/bin/env bash + + # start with a clean environment + module purge + + # isolate the container interpreter from your ~/.local site-packages + export APPTAINERENV_PYTHONNOUSERSITE=True + + # bind the directory holding the Jupyter connection file (the last + # argument) so the in-container interpreter can read it + for connection_file in "$@"; do :; done + connection_dir="$(dirname "$connection_file")" + + # run a python kernel inside the container + exec apptainer exec -B "$connection_dir" --nv python "$@" + ``` + + Make the wrapper script executable: + + ``` sh + chmod +x wrapper.sh + ``` + + Third, create a directory for your kernel: + + ``` sh + mkdir -p ~/.local/share/jupyter/kernels/my-container + cd ~/.local/share/jupyter/kernels/my-container + ``` + + and create a file called *kernel.json*. The file should look like this: + + ```json + { + "argv": [ + "/wrapper.sh", + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}" + ], + "display_name": "Container Name", + "language": "python" + } + ``` + + - Change `display_name` to what you would like to call your kernel by. + + After refreshing JupyterLab your new kernel should show up in the + Launcher under the display name you set in `kernel.json` (`Container + Name` in the example above). + + === "Bash kernel" + + This runs a **bash** kernel on the host that dispatches every cell into + the container (the manual equivalent of the `-cb` option). To do this you + first need to set up a Python virtual environment, in the same way as the + Manual Management approach described on the + [Python and R kernels in JupyterLab](./python_and_r_kernels_in_JupyterLab.md) + page. + + In brief, the steps below create a Python virtual environment, install + `bash_kernel` into it, write a wrapper script that points `bash_kernel` at + your container, and then register that wrapper as a Jupyter kernel. + + First, change directory into the path where you would like to place your + virtual environment. + + - If you would like to share it with other members of your project, use + your project folder (`cd /nesi/project/`). + - Avoid paths that include `00_nesi_projects` or `home`, as these cause + issues. + + Second, in a terminal run the following commands to load a Python + environment module: + + ``` sh + module purge + module load Python/3.14.4-foss-2026 + ``` + + Now create a Python virtual environment named `my-container-venv`. You can + change the name of the environment and install other Python packages as + required. + + ``` sh + python3 -m venv ./my-container-venv + source ./my-container-venv/bin/activate + pip install --upgrade pip + ``` + + Third, install `bash_kernel` in your virtual environment. + + ??? note "What is bash_kernel?" + + `bash_kernel` runs a bash shell as a Jupyter kernel. Here we point that bash shell at your container, so every cell you run in JupyterLab executes inside the container. It feels like a normal JupyterLab session, but the commands run in your container rather than on the host. + + ``` sh + pip install git+https://github.com/geoffreyweal/bash_kernel + ``` + + Fourth, create a wrapper for your virtual environment. Change directory + into your `my-container-venv` folder: + + ``` sh + cd my-container-venv + ``` + + And add the following as `wrapper.sh` into your `my-container-venv` folder: + + ``` sh + #!/usr/bin/env bash + + # load required modules here + module purge + module load Python/3.14.4-foss-2026 + + # activate the virtual environment that has bash_kernel installed + source /my-container-venv/bin/activate + + # run the kernel on the host (it dispatches every command into the container) + exec python3 "$@" + ``` + + Make the wrapper script executable: + + ``` sh + chmod +x wrapper.sh + ``` + + Fifth, create a directory for your kernel: + + ``` sh + mkdir -p ~/.local/share/jupyter/kernels/bash + cd ~/.local/share/jupyter/kernels/bash + ``` + + and create a file called *kernel.json*. The file should look like this: + + ```json + { + "argv": [ + "/wrapper.sh", + "-m", + "bash_kernel", + "-f", + "{connection_file}" + ], + "display_name": "Container Name", + "language": "bash", + "env": { + "BASH_KERNEL_CMD": "apptainer exec --nv bash" + } + } + ``` + + - Change `display_name` to what you would like to call your kernel by. + - Update the `apptainer exec` line in `BASH_KERNEL_CMD` to match your own + image, for example: + + ``` sh + "BASH_KERNEL_CMD": "apptainer exec --pwd /opt/PMDM --nv /nesi/project//containers/PMDM/pmdm_cu130.simg bash" + ``` + + After refreshing JupyterLab your new kernel should show up in the Launcher + under the display name you set in `kernel.json` (`Container Name` in the + example above). + +## Listing your kernels + +To list all the kernels that are currently installed, run: ``` sh module purge -module load Python/3.14.4-foss-2026 +module load JupyterLab +jupyter-kernelspec list ``` -Now create a Python virtual environment named `my-container-venv`. You can -change the name of the environment and install other Python packages as -required. +This works for any kernel, regardless of whether it was registered using the +Tool-Assisted Management or Manual Management approach. -``` sh -python3 -m venv ./my-container-venv -source ./my-container-venv/bin/activate -pip install --upgrade pip -``` +## Sharing a kernel -Third, install `bash_kernel` in your virtual environment. +You can also configure a shared container kernel that others with access to the +same project will be able to load. Whichever approach you use, make sure the +container image (and, for a bash kernel, the virtual environment and +`wrapper.sh`) live in a shared location such as your project folder +(`/nesi/project/`) — other users cannot read your home directory. +How you share depends on whether you registered the kernel with the +tool-assisted or manual approach: -??? note "What is bash_kernel?" +=== "Tool-Assisted Management" + + To share a container kernel, register it with `nesi-add-kernel` as you + normally would, but add the `--shared` flag. This installs the kernel into + your project's shared location, so other members of your project will see it + automatically. Select the tab for the kind of container kernel you are + sharing: - `bash_kernel` runs a bash shell as a Jupyter kernel. Here we point that bash shell at your container, so every cell you run in JupyterLab executes inside the container. It feels like a normal JupyterLab session, but the commands run in your container rather than on the host. + === "Python kernel" -``` sh -pip install bash_kernel -``` + ``` sh + nesi-add-kernel --shared -cp + ``` -Fourth, create a wrapper for your virtual environment. Change directory -into your `my-container-venv` folder: + Where `` is the name you want to give to the kernel. Note + the `--shared` flag in the `nesi-add-kernel` command line. -``` sh -cd my-container-venv -``` + === "Bash kernel" -And add the following as `wrapper.sh` into your `my-container-venv` folder: + ``` sh + nesi-add-kernel --shared -cb + ``` -``` sh -#!/usr/bin/env bash + Where `` is the name you want to give to the kernel. Note + the `--shared` flag in the `nesi-add-kernel` command line. -# load required modules here -module purge -module load Python/3.14.4-foss-2026 + !!! note -# activate the virtual environment that has bash_kernel installed -source /my-container-venv/bin/activate + Run `nesi-add-kernel --shared` from a terminal inside a Jupyter session + on OnDemand so it can detect your project. From a regular ssh session you + will also need to pass the project code with the `--account` option, for + example `nesi-add-kernel --shared --account ...`. -# tell bash_kernel to run inside your container instead of a normal bash shell -export BASH_KERNEL_CMD="apptainer exec --nv bash" +=== "Manual Management" -# run the kernel on the host (it dispatches every command into the container) -exec python3 "$@" -``` + For a manually-created kernel, **you** first create the kernel yourself, + following the manual steps in the **Manual Management** tab earlier on this + page, but making sure the container image and `wrapper.sh` (and, for a bash + kernel, the virtual environment) are all in your shared project folder. -The `BASH_KERNEL_CMD` environment variable is what makes this a *container* -kernel: `bash_kernel` itself runs on the host, but every cell you run in -JupyterLab is executed by the `bash` inside the container you point to. Update -the `apptainer exec` line to match your own image, for example: + Next, **your team members** set up the kernel on their side by creating the + kernelspec in their own home directory. The steps depend on the type of + container kernel you created, so follow the matching tab: -``` sh -export BASH_KERNEL_CMD="apptainer exec --pwd /opt/PMDM --nv /nesi/project//containers/PMDM/pmdm_cu130.simg bash" -``` + === "Python kernel" -Make the wrapper script executable: + **Your team members** create a directory for the kernel: -``` sh -chmod +x wrapper.sh -``` + ``` sh + mkdir -p ~/.local/share/jupyter/kernels/my-container + cd ~/.local/share/jupyter/kernels/my-container + ``` -Fifth, install `bash_kernel` based on your new virtual environment: + and create a file called *kernel.json* that points at the shared + `wrapper.sh`. The file should look like this: -``` sh -python3 -m bash_kernel.install --user -``` + ```json + { + "argv": [ + "/wrapper.sh", + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}" + ], + "display_name": "Container Name", + "language": "python" + } + ``` -This installs a kernel into `~/.local/share/jupyter/kernels/bash`. We must now -edit it so the kernel runs through the wrapper script we created above. Change -to the directory the kernelspec was installed to: + - Change `display_name` to what you would like to call your kernel by. -``` sh -cd ~/.local/share/jupyter/kernels/bash -``` + After refreshing JupyterLab the shared kernel should show up in the + Launcher. -and edit the *kernel.json* to change the first element of the argv list to point -to the wrapper script we just created. The file should look like this: - -```json -{ - "argv": [ - "/wrapper.sh", - "-m", - "bash_kernel", - "-f", - "{connection_file}" - ], - "display_name": "Container Name", - "language": "bash" -} -``` + === "Bash kernel" + + **Your team members** create a directory for the kernel: + + ``` sh + mkdir -p ~/.local/share/jupyter/kernels/bash + cd ~/.local/share/jupyter/kernels/bash + ``` + + and create a file called *kernel.json* that points at the shared + `wrapper.sh`. The file should look like this: + + ```json + { + "argv": [ + "/wrapper.sh", + "-m", + "bash_kernel", + "-f", + "{connection_file}" + ], + "display_name": "Container Name", + "language": "bash", + "env": { + "BASH_KERNEL_CMD": "apptainer exec --nv bash" + } + } + ``` + + - Change `display_name` to what you would like to call your kernel by. -After refreshing JupyterLab your new kernel should show up in the Launcher under -the display name you set in `kernel.json` (`Container Name` in the example -above). + After refreshing JupyterLab the shared kernel should show up in the + Launcher. + +## Removing a kernel + +To delete a specific kernel, run: + +``` sh +module purge +module load JupyterLab +jupyter-kernelspec list +jupyter-kernelspec remove +``` +where `` is the name of the kernel to delete, as shown by +`jupyter-kernelspec list`. diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md index 545fce5b4..3faa26b23 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/python_and_r_kernels_in_JupyterLab.md @@ -127,6 +127,13 @@ custom kernel; select the tab that suits you: Where `` is the name for your kernel. + === "Python environment through Container" + + You can also use the `nesi-add-kernel` tool to base a kernel on a + container. See the **Tool-Assisted Management → Python kernel** tab on the + [Containers as kernels in JupyterLab](./containers_as_kernels_in_JupyterLab.md) + page for the full instructions. + !!! tip For more information about `nesi-add-kernel`, type into the terminal: @@ -216,24 +223,15 @@ custom kernel; select the tab that suits you: chmod +x wrapper.sh ``` - Fourth, create a Jupyter kernel based on your new conda environment: - - ``` sh - python -m ipykernel install --user --name my-conda-env --display-name="My Conda Env" - ``` - - We must now edit the kernel to load the required environment - modules before the kernel is launched. Change to the directory the - kernelspec was installed to - `~/.local/share/jupyter/kernels/my-conda-env`, (assuming you kept - `--name my-conda-env` in the above command): + Fourth, create a directory for your kernel: ``` sh + mkdir -p ~/.local/share/jupyter/kernels/my-conda-env cd ~/.local/share/jupyter/kernels/my-conda-env ``` - and edit the *kernel.json* to change the first element of the argv list - to point to the wrapper script we just created. The file should look + and create a file called *kernel.json*. It points the first element of + the argv list at the wrapper script we just created. The file should look like this: ```json @@ -250,6 +248,8 @@ custom kernel; select the tab that suits you: } ``` + - Change `display_name` to what you would like to call your kernel by. + After refreshing JupyterLab your new kernel should show up in the Launcher as "My Conda Env". @@ -312,25 +312,15 @@ custom kernel; select the tab that suits you: chmod +x wrapper.sh ``` - Fourth, create a Jupyter kernel based on your new virtual environment: - - ``` sh - python -m ipykernel install --user --name my-venv --display-name="My Venv" - ``` - - We must now edit the kernel to load the required environment - modules before the kernel is launched. Change to the directory the - kernelspec was installed to - `~/.local/share/jupyter/kernels/my-venv`, (assuming you kept - `--name my-venv` in the above command): + Fourth, create a directory for your kernel: ``` sh mkdir -p ~/.local/share/jupyter/kernels/my-venv cd ~/.local/share/jupyter/kernels/my-venv ``` - and edit the *kernel.json* to change the first element of the argv list - to point to the wrapper script we just created. The file should look + and create a file called *kernel.json*. It points the first element of + the argv list at the wrapper script we just created. The file should look like this: ```json @@ -347,6 +337,8 @@ custom kernel; select the tab that suits you: } ``` + - Change `display_name` to what you would like to call your kernel by. + After refreshing JupyterLab your new kernel should show up in the Launcher as "My Venv". @@ -426,9 +418,18 @@ custom kernel; select the tab that suits you: } ``` + - Change `display_name` to what you would like to call your kernel by. + After refreshing JupyterLab your new R kernel should show up in the Launcher as "R with MPFR". + === "Python environment through Container" + + You can also set up a kernel by hand that uses a container's version of + Python. See the **Manual Management → Python kernel** tab on the + [Containers as kernels in JupyterLab](./containers_as_kernels_in_JupyterLab.md) + page for the full instructions. + ## Listing your kernels To list all the kernels that are currently installed, run: @@ -582,25 +583,16 @@ the kernel with the tool-assisted or manual approach: conda activate ./my-conda-env ``` - Third, get **your team members** to create a Jupyter kernel based on your - conda environment: - - ``` sh - python -m ipykernel install --user --name my-conda-env --display-name="My Conda Env" - ``` - - **Your project members** must now edit the kernel in their home directories - to load the required environment modules before the kernel is launched. - Change to the directory the kernelspec was installed to - `~/.local/share/jupyter/kernels/my-conda-env`, (assuming you kept - `--name my-conda-env` in the above command): + Third, get **your team members** to create a directory for the kernel in + their home directories: ``` sh + mkdir -p ~/.local/share/jupyter/kernels/my-conda-env cd ~/.local/share/jupyter/kernels/my-conda-env ``` - and edit the *kernel.json* to change the first element of the argv list - to point to the wrapper script we just created. The file should look + and create a file called *kernel.json*. It points the first element of + the argv list at the wrapper script we just created. The file should look like this: ```json @@ -617,6 +609,8 @@ the kernel with the tool-assisted or manual approach: } ``` + - Change `display_name` to what you would like to call your kernel by. + After refreshing JupyterLab your new kernel should show up in the Launcher as "My Conda Env". @@ -634,26 +628,16 @@ the kernel with the tool-assisted or manual approach: source /my-venv/bin/activate ``` - Third, get **your team members** to create a Jupyter kernel based on your - virtual environment: - - ``` sh - python -m ipykernel install --user --name my-venv --display-name="My Venv" - ``` - - **Your project members** must now edit the kernel in their home directories - to load the required environment modules before the kernel is launched. - Change to the directory the kernelspec was installed to - `~/.local/share/jupyter/kernels/my-venv`, (assuming you kept - `--name my-venv` in the above command): + Third, get **your team members** to create a directory for the kernel in + their home directories: ``` sh mkdir -p ~/.local/share/jupyter/kernels/my-venv cd ~/.local/share/jupyter/kernels/my-venv ``` - and edit the *kernel.json* to change the first element of the argv list - to point to the wrapper script we just created. The file should look + and create a file called *kernel.json*. It points the first element of + the argv list at the wrapper script we just created. The file should look like this: ```json @@ -670,6 +654,8 @@ the kernel with the tool-assisted or manual approach: } ``` + - Change `display_name` to what you would like to call your kernel by. + After refreshing JupyterLab your new kernel should show up in the Launcher as "My Venv". From 7536a91f747ec3d87b5e144b364af801d61a4698 Mon Sep 17 00:00:00 2001 From: geoffreyweal Date: Tue, 23 Jun 2026 13:14:05 +1200 Subject: [PATCH 09/10] Update to give warning indicating that the user needs to make sure they are using slurm and using an updated version of JupyterLab --- .../containers_as_kernels_in_JupyterLab.md | 9 +++++++++ .../images/OOD_jupyter_form_containers.png | Bin 0 -> 73629 bytes docs/assets/stylesheets/theme.css | 7 +++++++ 3 files changed, 16 insertions(+) create mode 100644 docs/assets/images/OOD_jupyter_form_containers.png diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md index 2cf9bede2..fa9c2a276 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md @@ -17,6 +17,15 @@ There are two ways to run a container as a kernel; select the tab that suits you - **Manual Management**: you set up the kernel and its wrapper script by hand. This can be useful when you need more control over how the kernel is launched. +!!! warning + + To use containers you must launch your Jupyter session with **Cluster** set + to **Slurm HPC** and the **JupyterLab module** set to + **2026.7.0-foss-2026-4.6.0**. If you do not see **Slurm HPC** in the + **Cluster** list, [get in touch](mailto:support@nesi.org.nz). + + ![JupyterLab launch form with Cluster set to Slurm HPC and JupyterLab module set to 2026.7.0-foss-2026-4.6.0](../../../../assets/images/OOD_jupyter_form_containers.png){.center width="400"} + === "Tool-Assisted Management" The `nesi-add-kernel` tool can register a container as a kernel for you. diff --git a/docs/assets/images/OOD_jupyter_form_containers.png b/docs/assets/images/OOD_jupyter_form_containers.png new file mode 100644 index 0000000000000000000000000000000000000000..8530ddb9bc5e7de5b572be61d121550ab096fc10 GIT binary patch literal 73629 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 literal 0 HcmV?d00001 diff --git a/docs/assets/stylesheets/theme.css b/docs/assets/stylesheets/theme.css index c79866038..8f701c94a 100644 --- a/docs/assets/stylesheets/theme.css +++ b/docs/assets/stylesheets/theme.css @@ -431,3 +431,10 @@ div.quiz-feedback { text-wrap-mode: wrap; font-size: medium; } + +/* Centre images marked with {.center} */ +img.center { + display: block; + margin-left: auto; + margin-right: auto; +} From d38c00d453893c0052978be09bba50de0c44a659 Mon Sep 17 00:00:00 2001 From: geoffreyweal Date: Tue, 23 Jun 2026 13:54:59 +1200 Subject: [PATCH 10/10] centering now working --- .../Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md index fa9c2a276..a7ca83aac 100644 --- a/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md +++ b/docs/Interactive_Computing/OnDemand/Apps/JupyterLab/containers_as_kernels_in_JupyterLab.md @@ -24,7 +24,7 @@ There are two ways to run a container as a kernel; select the tab that suits you **2026.7.0-foss-2026-4.6.0**. If you do not see **Slurm HPC** in the **Cluster** list, [get in touch](mailto:support@nesi.org.nz). - ![JupyterLab launch form with Cluster set to Slurm HPC and JupyterLab module set to 2026.7.0-foss-2026-4.6.0](../../../../assets/images/OOD_jupyter_form_containers.png){.center width="400"} +

JupyterLab launch form with Cluster set to Slurm HPC and JupyterLab module set to 2026.7.0-foss-2026-4.6.0

=== "Tool-Assisted Management"