From 547baa9a774e72eabedd405e80879030e2c432b0 Mon Sep 17 00:00:00 2001 From: vmensik Date: Sun, 14 May 2023 13:15:52 +0200 Subject: [PATCH] Use optimized numpy functions and thread count limiting --- Matrix_Multiplication.ipynb | 499 ++++++++++++++++++------------------ 1 file changed, 255 insertions(+), 244 deletions(-) diff --git a/Matrix_Multiplication.ipynb b/Matrix_Multiplication.ipynb index 8030e0c..76a6a60 100644 --- a/Matrix_Multiplication.ipynb +++ b/Matrix_Multiplication.ipynb @@ -1,254 +1,265 @@ { - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "colab": { - "provenance": [], - "gpuType": "T4" - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - }, - "language_info": { - "name": "python" - }, - "accelerator": "GPU", - "gpuClass": "standard" - }, - "cells": [ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "id": "90tUQ4ralGOO" - }, - "outputs": [], - "source": [ - "def matmul_cpu(A, B):\n", - " m, k = A.shape\n", - " k, n = B.shape\n", - " C = np.zeros((m, n))\n", - " for i in range(m):\n", - " for j in range(n):\n", - " for l in range(k):\n", - " C[i, j] += A[i, l] * B[l, j]\n", - " return C\n" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: threadpoolctl in c:\\users\\vasek\\appdata\\local\\programs\\python\\python311\\lib\\site-packages (3.1.0)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install threadpoolctl" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import pycuda.driver as cuda\n", + "import pycuda.autoinit\n", + "from pycuda.compiler import SourceModule\n", + "\n", + "mod = SourceModule(\"\"\"\n", + " __global__ void matmul(float *A, float *B, float *C, int m, int k, int n) {\n", + " int i = blockIdx.x * blockDim.x + threadIdx.x;\n", + " int j = blockIdx.y * blockDim.y + threadIdx.y;\n", + " if (i < m && j < n) {\n", + " float sum = 0;\n", + " for (int l = 0; l < k; l++) {\n", + " sum += A[i * k + l] * B[l * n + j];\n", + " }\n", + " C[i * n + j] = sum;\n", + " }\n", + " }\n", + "\"\"\")\n", + "\n", + "def matmul_gpu(A, B):\n", + " m, k = A.shape\n", + " k, n = B.shape\n", + " C = np.zeros((m, n)).astype(np.float32)\n", + "\n", + " block_size = (32, 32,1)\n", + " grid_size = ((m + block_size[0] - 1) // block_size[0], (n + block_size[1] - 1) // block_size[1])\n", + "\n", + " func = mod.get_function(\"matmul\")\n", + " func(cuda.In(A), cuda.In(B), cuda.Out(C), np.int32(m), np.int32(k), np.int32(n), block=block_size, grid=grid_size)\n", + "\n", + " return C" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def matmul_cpu_at_operator(A, B):\n", + " C = A @ B\n", + " return C" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def matmul_cpu_dot(A, B):\n", + " C = np.dot(A, B)\n", + " return C" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# set the number of threads for many common libraries\n", + "from os import environ\n", + "# use your number of physical cores\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "source": [ - "!pip install pycuda\n", - "import pycuda.driver as cuda\n", - "import pycuda.autoinit\n", - "from pycuda.compiler import SourceModule\n", - "\n", - "mod = SourceModule(\"\"\"\n", - " __global__ void matmul(float *A, float *B, float *C, int m, int k, int n) {\n", - " int i = blockIdx.x * blockDim.x + threadIdx.x;\n", - " int j = blockIdx.y * blockDim.y + threadIdx.y;\n", - " if (i < m && j < n) {\n", - " float sum = 0;\n", - " for (int l = 0; l < k; l++) {\n", - " sum += A[i * k + l] * B[l * n + j];\n", - " }\n", - " C[i * n + j] = sum;\n", - " }\n", - " }\n", - "\"\"\")\n", - "\n", - "def matmul_gpu(A, B):\n", - " m, k = A.shape\n", - " k, n = B.shape\n", - " C = np.zeros((m, n)).astype(np.float32)\n", - "\n", - " block_size = (32, 32,1)\n", - " grid_size = ((m + block_size[0] - 1) // block_size[0], (n + block_size[1] - 1) // block_size[1])\n", - "\n", - " func = mod.get_function(\"matmul\")\n", - " func(cuda.In(A), cuda.In(B), cuda.Out(C), np.int32(m), np.int32(k), np.int32(n), block=block_size, grid=grid_size)\n", - "\n", - " return C\n" - ], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "rILR4M55lL36", - "outputId": "34418602-1b38-482a-a5db-ac2512e41710" - }, - "execution_count": 2, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", - "Collecting pycuda\n", - " Downloading pycuda-2022.2.2.tar.gz (1.7 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.7/1.7 MB\u001b[0m \u001b[31m47.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25h Installing build dependencies ... \u001b[?25l\u001b[?25hdone\n", - " Getting requirements to build wheel ... \u001b[?25l\u001b[?25hdone\n", - " Preparing metadata (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n", - "Collecting pytools>=2011.2 (from pycuda)\n", - " Downloading pytools-2022.1.14.tar.gz (74 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m74.6/74.6 kB\u001b[0m \u001b[31m3.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25h Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n", - "Requirement already satisfied: appdirs>=1.4.0 in /usr/local/lib/python3.10/dist-packages (from pycuda) (1.4.4)\n", - "Collecting mako (from pycuda)\n", - " Downloading Mako-1.2.4-py3-none-any.whl (78 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m78.7/78.7 kB\u001b[0m \u001b[31m11.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hRequirement already satisfied: platformdirs>=2.2.0 in /usr/local/lib/python3.10/dist-packages (from pytools>=2011.2->pycuda) (3.3.0)\n", - "Requirement already satisfied: typing_extensions>=4.0 in /usr/local/lib/python3.10/dist-packages (from pytools>=2011.2->pycuda) (4.5.0)\n", - "Requirement already satisfied: MarkupSafe>=0.9.2 in /usr/local/lib/python3.10/dist-packages (from mako->pycuda) (2.1.2)\n", - "Building wheels for collected packages: pycuda, pytools\n", - " Building wheel for pycuda (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n", - " Created wheel for pycuda: filename=pycuda-2022.2.2-cp310-cp310-linux_x86_64.whl size=661975 sha256=9cbd3823108ef6058e441285112d917df97b4796d341d252504bca2e588f3ba0\n", - " Stored in directory: /root/.cache/pip/wheels/1d/7b/06/82a395a243fce00035dea9914d92bbef0013401497d849f8bc\n", - " Building wheel for pytools (setup.py) ... \u001b[?25l\u001b[?25hdone\n", - " Created wheel for pytools: filename=pytools-2022.1.14-py2.py3-none-any.whl size=69855 sha256=e9e23c683d0fb9e55423a964cd955e956bfee5295fd0d904a42f28f3257a795e\n", - " Stored in directory: /root/.cache/pip/wheels/19/02/16/aa2498ad7aa723a149ff7539f1918509661c0ae9d975b44b6d\n", - "Successfully built pycuda pytools\n", - "Installing collected packages: pytools, mako, pycuda\n", - "Successfully installed mako-1.2.4 pycuda-2022.2.2 pytools-2022.1.14\n" - ] - } - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Zero matrix init: 0.238 s\n", + "CPU time @ operator: 4.195 s\n", + "CPU time np.dot: 4.224 s\n", + "GPU time: 10.136 s\n" + ] + } + ], + "source": [ + "import time\n", + "import numpy as np\n", + "# Generate random matrices\n", + "A = np.random.rand(10000,10000).astype(np.float32)\n", + "B = np.random.rand(10000,10000).astype(np.float32)\n", + "\n", + "start_time = time.time()\n", + "m, k = A.shape\n", + "k, n = B.shape\n", + "C = np.zeros((m, n)).astype(np.float32)\n", + "end_time = time.time()\n", + "zeros_time = end_time - start_time\n", + "print(\"Zero matrix init: {:.3f} s\".format(end_time - start_time))\n", + "\n", + "# Time CPU-only implementation \n", + "#start_time = time.time()\n", + "#C_cpu_at_operator = matmul_cpu_at_operator(A, B)\n", + "#end_time = time.time()\n", + "#cpu2_time = end_time - start_time\n", + "#print(\"CPU time np: {:.3f} s\".format(end_time - start_time))\n", + "\n", + "from threadpoolctl import threadpool_limits\n", + "\n", + "N_THREADS = 8\n", + "with threadpool_limits(limits=N_THREADS, user_api='blas'):\n", + " # Time CPU-only implementation with @ symbol\n", + " start_time = time.time()\n", + " C_cpu_at = matmul_cpu_at_operator(A, B)\n", + " end_time = time.time()\n", + " cpu_at_time = end_time - start_time\n", + " print(\"CPU time @ operator: {:.3f} s\".format(end_time - start_time))\n", + "\n", + "with threadpool_limits(limits=N_THREADS, user_api='blas'):\n", + "# Time CPU-only implementation with np.dot\n", + " start_time = time.time()\n", + " C_cpu_dot = matmul_cpu_dot(A, B)\n", + " end_time = time.time()\n", + " cpu_dot_time = end_time - start_time\n", + " print(\"CPU time np.dot: {:.3f} s\".format(end_time - start_time))\n", + "\n", + "# Time PyCUDA-accelerated implementation\n", + "start_time = time.time()\n", + "C_gpu = matmul_gpu(A, B)\n", + "end_time = time.time()\n", + "gpu_time = end_time - start_time\n", + "print(\"GPU time: {:.3f} s\".format(end_time - start_time))\n", + "\n", + "# Check that results are the same\n", + "assert np.allclose(C_cpu_at, C_gpu)\n", + "assert np.allclose(C_cpu_dot, C_gpu)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "source": [ - "import numpy as np\n", - "import time\n", - "\n", - "# Generate random matrices\n", - "A = np.random.rand(300,300).astype(np.float32)\n", - "B = np.random.rand(300,300).astype(np.float32)\n", - "\n", - "# Time CPU-only implementation\n", - "start_time = time.time()\n", - "C_cpu = matmul_cpu(A, B)\n", - "end_time = time.time()\n", - "cpu_time = end_time - start_time\n", - "print(\"CPU time: {:.3f} s\".format(end_time - start_time))\n", - "\n", - "# Time PyCUDA-accelerated implementation\n", - "start_time = time.time()\n", - "C_gpu = matmul_gpu(A, B)\n", - "end_time = time.time()\n", - "gpu_time = end_time - start_time\n", - "print(\"GPU time: {:.3f} s\".format(end_time - start_time))\n", - "\n", - "# Check that results are the same\n", - "assert np.allclose(C_cpu, C_gpu)\n" - ], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "QeEhB0AplNni", - "outputId": "3499a792-f68f-44a8-c2c5-e782e81a2082" - }, - "execution_count": 15, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "CPU time: 18.514 s\n", - "GPU time: 0.004 s\n" - ] - } - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU time: 4.651 s\n", + "GPU time: 10.224 s\n" + ] }, { - "cell_type": "code", - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "# ...\n", - "\n", - "# Time CPU-only implementation\n", - "start_time = time.time()\n", - "C_cpu = matmul_cpu(A, B)\n", - "end_time = time.time()\n", - "cpu_time = end_time - start_time\n", - "print(\"CPU time: {:.3f} s\".format(cpu_time))\n", - "\n", - "# Time PyCUDA-accelerated implementation\n", - "start_time = time.time()\n", - "C_gpu = matmul_gpu(A, B)\n", - "end_time = time.time()\n", - "gpu_time = end_time - start_time\n", - "print(\"GPU time: {:.3f} s\".format(gpu_time))\n", - "\n", - "# Calculate the time difference\n", - "time_diff = cpu_time - gpu_time\n", - "\n", - "# Plot the time difference\n", - "labels = ['CPU', 'GPU']\n", - "times = [cpu_time, gpu_time]\n", - "colors = ['red', 'green']\n", - "plt.bar(labels, times, color=colors)\n", - "plt.xlabel('Implementation')\n", - "plt.ylabel('Time (s)')\n", - "plt.title('Running Time Comparison')\n", - "\n", - "# Add time labels to the plot\n", - "for i in range(len(labels)):\n", - " plt.text(i, times[i], '{} ({:.3f}s)'.format(labels[i], times[i]), ha='center', va='bottom')\n", - "\n", - "plt.show()\n", - "\n", - "# Print the time difference\n", - "print(\"Time Difference: {:.3f} s\".format(time_diff))\n" - ], - "metadata": { - "id": "tcbNE7Xrqqzg", - "colab": { - "base_uri": "https://localhost:8080/", - "height": 524 - }, - "outputId": "e907c196-9258-441c-8a8c-26b2227af514" - }, - "execution_count": 16, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "CPU time: 19.892 s\n", - "GPU time: 0.003 s\n" - ] - }, - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAHHCAYAAABXx+fLAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABKGUlEQVR4nO3deVhV1f7H8c9hRhBMQQZFFDE15yEcu2oOZGYOpf7MUtTqllqZV1MbNLXCssEcsuGWWFaWmTaKqamZQ+ZAOZI4W0A5AIoKCuv3R4/ndgQUEATc79fz7Odxr73WOt99FM7HPR2bMcYIAADAQpxKugAAAIBrjQAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEWIzNZtOzzz5b0mXkKSYmRjabTQcPHizpUvAPUVFRql69ekmXARQZAhBQBC5+aF9cXFxcVKVKFUVFRen3338v6fJKXPv27R3en7yW0hzMzp07p9dee00tWrSQr6+vPDw8dOONN2rEiBH67bffSro8AAVk47vAgKsXExOjwYMHa/LkyapRo4bOnTunjRs3KiYmRtWrV9eOHTvk4eFR0mVK+vuD3MXFRS4uLtfsNZcvX67k5GT7+s8//6wZM2boySefVN26de3tDRs2VL169XT+/Hm5u7vLZrNdsxov59ixY7rtttu0ZcsW3XHHHerUqZO8vb0VHx+vBQsWKCkpSZmZmSVdZrE6f/68srOz5e7uXtKlAEXi2v0GBCyga9euat68uSTp/vvvl5+fn1588UV9+eWX6tu3bwlX97eSCGKdO3fOUcOMGTPUuXNntW/fPkd/Z2fna1RZ/kRFRWnbtm367LPPdNdddzlsmzJlip566qkSqqz4paeny8vLS66uriVdClCkOAUGFKNbbrlFkrRv3z57W/v27XP90L/0GouDBw/KZrPp5Zdf1ttvv62aNWvK3d1dN998s37++eccY729vfX777+rZ8+e8vb2lr+/v0aPHq2srCyHvpeeanr22Wdls9mUkJCgqKgoVahQQb6+vho8eLDOnDnjMPbs2bN69NFH5efnp/Lly+vOO+/U77//XqSnr3K7Bqh69eq64447tHr1ajVv3lyenp5q0KCBVq9eLUn6/PPP1aBBA3l4eKhZs2batm1bjnn37Nmju+++WxUrVpSHh4eaN2+uL7/88or1/PTTT/rmm280dOjQHOFHktzd3fXyyy87tH3//fe65ZZb5OXlpQoVKqhHjx7avXu3Q5+L7/tvv/2me++9V76+vvL399czzzwjY4yOHDmiHj16yMfHR4GBgXrllVccxq9evVo2m02ffPKJnnzySQUGBsrLy0t33nmnjhw54tB37dq16tOnj6pVqyZ3d3eFhITo8ccf19mzZx36Xfx3tG/fPt1+++0qX768BgwYYN926TVACxYsULNmzVS+fHn5+PioQYMGev311x367N+/X3369FHFihVVrlw5tWzZUt98802u+/Lpp5/q+eefV9WqVeXh4aGOHTsqISEhj78Z4OoQgIBidPFD/IYbbij0HB999JGmTZumf//733ruued08OBB9e7dW+fPn3fol5WVpcjISFWqVEkvv/yy2rVrp1deeUVvv/12vl6nb9++OnXqlKKjo9W3b1/FxMRo0qRJDn2ioqI0c+ZM3X777XrxxRfl6empbt26FXrfCiIhIUH33HOPunfvrujoaJ08eVLdu3fXhx9+qMcff1z33nuvJk2apH379qlv377Kzs62j925c6datmyp3bt3a9y4cXrllVfk5eWlnj17avHixZd93Ysh6b777stXnStWrFBkZKT+/PNPPfvssxo1apTWr1+vNm3a5Hphd79+/ZSdna2pU6eqRYsWeu655zR9+nR17txZVapU0Ysvvqjw8HCNHj1aP/zwQ47xzz//vL755huNHTtWjz76qJYvX65OnTo5hJuFCxfqzJkzevjhhzVz5kxFRkZq5syZGjhwYI75Lly4oMjISFWuXFkvv/xyrqFP+vu0Zv/+/XXDDTfoxRdf1NSpU9W+fXutW7fO3ic5OVmtW7fWsmXLNGzYMD3//PM6d+6c7rzzzlzf96lTp2rx4sUaPXq0xo8fr40bN9oDGFDkDICrNnfuXCPJrFixwvz111/myJEj5rPPPjP+/v7G3d3dHDlyxN63Xbt2pl27djnmGDRokAkNDbWvHzhwwEgylSpVMidOnLC3f/HFF0aS+eqrrxzGSjKTJ092mLNJkyamWbNmDm2SzMSJE+3rEydONJLMkCFDHPr16tXLVKpUyb6+ZcsWI8mMHDnSoV9UVFSOOa9k4cKFRpJZtWpVjm0X38sDBw7Y20JDQ40ks379envbsmXLjCTj6elpDh06ZG9/6623cszdsWNH06BBA3Pu3Dl7W3Z2tmndurWpVavWZWvt1auXkWROnjyZr31r3LixqVy5sjl+/Li97ZdffjFOTk5m4MCB9raL7/uDDz5ob7tw4YKpWrWqsdlsZurUqfb2kydPGk9PTzNo0CB726pVq4wkU6VKFZOWlmZv//TTT40k8/rrr9vbzpw5k6PO6OhoY7PZHN67i/+Oxo0bl6P/pf8+H3vsMePj42MuXLiQ53sxcuRII8msXbvW3nbq1ClTo0YNU716dZOVleWwL3Xr1jUZGRn2vq+//rqRZLZv357nawCFxREgoAh16tRJ/v7+CgkJ0d133y0vLy99+eWXqlq1aqHn7Nevn8MRpIun1fbv35+j70MPPeSwfsstt+TaLze5jT1+/LjS0tIkSbGxsZKkYcOGOfR75JFH8jX/1brpppvUqlUr+3qLFi0kSbfeequqVauWo/3ifp84cULff/+9/QjXsWPHdOzYMR0/flyRkZHau3fvZe/Uu7j/5cuXv2KNiYmJiouLU1RUlCpWrGhvb9iwoTp37qxvv/02x5j777/f/mdnZ2c1b95cxhgNHTrU3l6hQgXVrl0717/LgQMHOtR29913KygoyOG1PD097X9OT0/XsWPH1Lp1axljcj1d+PDDD19xXytUqKD09HQtX748zz7ffvutIiIi1LZtW3ubt7e3HnzwQR08eFC7du1y6D948GC5ubnZ1y/3bx24WgQgoAjNnj1by5cv12effabbb79dx44du+q7Zv754S7973TayZMnHdo9PDzk7++fo++l/Qr7OocOHZKTk5Nq1Kjh0C88PDxf81+tS+vz9fWVJIWEhOTafrHuhIQEGWP0zDPPyN/f32GZOHGiJOnPP//M83V9fHwkSadOnbpijYcOHZIk1a5dO8e2unXr6tixY0pPT7/ifnl4eMjPzy9He25/l7Vq1XJYt9lsCg8PdzjddvjwYXsou3h9WLt27SRJqampDuNdXFzyFdiHDRumG2+8UV27dlXVqlU1ZMgQe0i+6NChQ3m+Fxe3/1N+/60DRYG7wIAiFBERYb8LrGfPnmrbtq3uuecexcfHy9vbW9LfH1Aml6dPXHqx8kV53RF16RxXe+dUfl+npORV35Xqvngt0OjRoxUZGZlr38uFuDp16kiStm/fbj8iUZRyq78o/y6ysrLUuXNnnThxQmPHjlWdOnXk5eWl33//XVFRUQ7XSkl/X9Tt5HTl/xtXrlxZcXFxWrZsmZYuXaqlS5dq7ty5GjhwoObNm1fgOqXS/28Q1xcCEFBMnJ2dFR0drQ4dOmjWrFkaN26cpL//V5vbIf1L/zdc2oSGhio7O1sHDhxwOOpQ2u/SCQsLkyS5urqqU6dOBR5/8aLr+fPnXzEAhYaGSpLi4+NzbNuzZ4/8/Pzk5eVV4BouZ+/evQ7rxhglJCSoYcOGkv4Obr/99pvmzZvncNHz5U5d5Zebm5u6d++u7t27Kzs7W8OGDdNbb72lZ555RuHh4QoNDc3zvZD+934BJYFTYEAxat++vSIiIjR9+nSdO3dOklSzZk3t2bNHf/31l73fL7/84nD3TGl08ejJG2+84dA+c+bMkign3ypXrqz27dvrrbfeUmJiYo7t//x7yE2rVq1022236b///a+WLFmSY3tmZqZGjx4tSQoKClLjxo01b948paSk2Pvs2LFD3333nW6//far2pfcvP/++w6n5z777DMlJiaqa9eukv53VOWfR1GMMTluVy+o48ePO6w7OTnZQ1dGRoYk6fbbb9emTZu0YcMGe7/09HS9/fbbql69um666aarqgG4GhwBAorZmDFj1KdPH8XExOihhx7SkCFD9OqrryoyMlJDhw7Vn3/+qTfffFP16tWzX3BbGjVr1kx33XWXpk+fruPHj6tly5Zas2aN/WsgSstTm3Mze/ZstW3bVg0aNNADDzygsLAwJScna8OGDTp69Kh++eWXy45///331aVLF/Xu3Vvdu3dXx44d5eXlpb1792rBggVKTEy0Pwto2rRp6tq1q1q1aqWhQ4fq7Nmzmjlzpnx9fYvlqz4qVqyotm3bavDgwUpOTtb06dMVHh6uBx54QNLfp/Bq1qyp0aNH6/fff5ePj48WLVp01dfV3H///Tpx4oRuvfVWVa1aVYcOHdLMmTPVuHFj+zU+48aN08cff6yuXbvq0UcfVcWKFTVv3jwdOHBAixYtytepNqC48K8PKGa9e/dWzZo19fLLLysrK0t169bV+++/r9TUVI0aNUpffvmlPvjgAzVt2rSkS72i999/X8OHD7c/dyYzM1OffPKJpJJ5wnR+3XTTTdq8ebO6deummJgYDR8+XG+++aacnJw0YcKEK4739/fX+vXrNW3aNCUmJuqpp57SsGHD9Pnnn+vOO+90uJupU6dOio2NVaVKlTRhwgS9/PLLatmypdatW5fjAvKi8OSTT6pbt26Kjo7W66+/ro4dO2rlypUqV66cpL9P/X311Vdq3LixoqOjNWnSJNWqVUvvv//+Vb3uvffeKw8PD73xxhsaNmyY5s2bp379+mnp0qX2YBMQEKD169erc+fOmjlzpsaPHy83Nzd99dVX6tWr11XvO3A1+C4wAFclLi5OTZo00fz583lo3TW0evVqdejQQQsXLtTdd99d0uUAZQ5HgADk26VfnSBJ06dPl5OTk/71r3+VQEUAUDhcAwQg31566SVt2bJFHTp0kIuLi/325wcffDDH83gAoDQjAAHIt9atW2v58uWaMmWKTp8+rWrVqunZZ5+9rr8NHcD1iWuAAACA5XANEAAAsBwCEAAAsByuAcpFdna2/vjjD5UvX75UP9wNAAD8jzFGp06dUnBw8BUftEkAysUff/zBHS0AAJRRR44cUdWqVS/bhwCUi/Lly0v6+w308fEp4WoAAEB+pKWlKSQkxP45fjkEoFxcPO3l4+NDAAIAoIzJz+UrXAQNAAAshwCE68K7776rLl26lHQZ10zLli21aNGiki4DAMosAhCuWlJSkh555BGFhYXJ3d1dISEh6t69u1auXGnvU716ddlsNtlsNnl5ealp06ZauHChfXtUVJR69uyZY+7Vq1fLZrMpJSUlz9c/d+6cnnnmGU2cONHetnPnTt111132150+fXqOcadOndLIkSMVGhoqT09PtW7dWj///PMV9/fDDz9Uo0aNVK5cOQUFBWnIkCE6fvy4ffv58+c1efJk1axZUx4eHmrUqJFiY2Md5oiOjtbNN9+s8uXLq3LlyurZs6fi4+Ov+NoXPf300xo3bpyys7PzPQYA8D8EIFyVgwcPqlmzZvr+++81bdo0bd++XbGxserQoYOGDx/u0Hfy5MlKTEzUtm3bdPPNN6tfv35av379Vdfw2WefycfHR23atLG3nTlzRmFhYZo6daoCAwNzHXf//fdr+fLl+uCDD7R9+3Z16dJFnTp10u+//57na61bt04DBw7U0KFDtXPnTi1cuFCbNm3SAw88YO/z9NNP66233tLMmTO1a9cuPfTQQ+rVq5e2bdtm77NmzRoNHz5cGzdu1PLly3X+/Hl16dJF6enp+drnrl276tSpU1q6dGm++gMALmGQQ2pq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zWEBAADXrzITgObNm6fy5curd+/el+3XokULxcTEKDY2VnPmzNGBAwd0yy236NSpU3mOiY6Olq+vr30JCQkp6vIBAEApYjPGmJIuQvr7gufFixerZ8+euW6vU6eOOnfurJkzZxZo3pSUFIWGhurVV1/N8+hRRkaGMjIy7OtpaWkKCQlRamqqfHx8CvR6+WKzFf2cwPWidPxKAlAGpaWlydfXN1+f3yV6DVB+rV27VvHx8frkk08KPLZChQq68cYblZCQkGcfd3d3ubu7X02JAACgDCkTp8DeffddNWvWTI0aNSrw2NOnT2vfvn0KCgoqhsoAAEBZVKIB6PTp04qLi1NcXJwk6cCBA4qLi3O4aDktLU0LFy7U/fffn+scHTt21KxZs+zro0eP1po1a3Tw4EGtX79evXr1krOzs/r371+s+wIAAMqOEj0FtnnzZnXo0MG+PmrUKEnSoEGDFBMTI0lasGCBjDF5Bph9+/bp2LFj9vWjR4+qf//+On78uPz9/dW2bVtt3LhR/v7+xbcjAACgTCk1F0GXJgW5iKpQuAgayBu/kgAUUkE+v8vENUAAAABFiQAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAshwAEAAAsp0QD0A8//KDu3bsrODhYNptNS5YscdgeFRUlm83msNx2221XnHf27NmqXr26PDw81KJFC23atKmY9gAAAJRFJRqA0tPT1ahRI82ePTvPPrfddpsSExPty8cff3zZOT/55BONGjVKEydO1NatW9WoUSNFRkbqzz//LOryAQBAGeVSki/etWtXde3a9bJ93N3dFRgYmO85X331VT3wwAMaPHiwJOnNN9/UN998o/fee0/jxo27qnoBAMD1odRfA7R69WpVrlxZtWvX1sMPP6zjx4/n2TczM1NbtmxRp06d7G1OTk7q1KmTNmzYkOe4jIwMpaWlOSwAAOD6VaoD0G233ab3339fK1eu1Isvvqg1a9aoa9euysrKyrX/sWPHlJWVpYCAAIf2gIAAJSUl5fk60dHR8vX1tS8hISFFuh8AAKB0KdFTYFfyf//3f/Y/N2jQQA0bNlTNmjW1evVqdezYscheZ/z48Ro1apR9PS0tjRAEAMB1rFQfAbpUWFiY/Pz8lJCQkOt2Pz8/OTs7Kzk52aE9OTn5stcRubu7y8fHx2EBAADXrzIVgI4eParjx48rKCgo1+1ubm5q1qyZVq5caW/Lzs7WypUr1apVq2tVJgAAKOVKNACdPn1acXFxiouLkyQdOHBAcXFxOnz4sE6fPq0xY8Zo48aNOnjwoFauXKkePXooPDxckZGR9jk6duyoWbNm2ddHjRqld955R/PmzdPu3bv18MMPKz093X5XGAAAQIleA7R582Z16NDBvn7xOpxBgwZpzpw5+vXXXzVv3jylpKQoODhYXbp00ZQpU+Tu7m4fs2/fPh07dsy+3q9fP/3111+aMGGCkpKS1LhxY8XGxua4MBoAAFiXzRhjSrqI0iYtLU2+vr5KTU0tnuuBbLainxO4XvArCUAhFeTzu0xdAwQAAFAUCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMBySjQA/fDDD+revbuCg4Nls9m0ZMkS+7bz589r7NixatCggby8vBQcHKyBAwfqjz/+uOyczz77rGw2m8NSp06dYt4TAABQlpRoAEpPT1ejRo00e/bsHNvOnDmjrVu36plnntHWrVv1+eefKz4+XnfeeecV561Xr54SExPty48//lgc5QMAgDLKpSRfvGvXruratWuu23x9fbV8+XKHtlmzZikiIkKHDx9WtWrV8pzXxcVFgYGBRVorAAC4fpSpa4BSU1Nls9lUoUKFy/bbu3evgoODFRYWpgEDBujw4cPXpkAAAFAmlOgRoII4d+6cxo4dq/79+8vHxyfPfi1atFBMTIxq166txMRETZo0Sbfccot27Nih8uXL5zomIyND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X6U6ALVo0UIxMTGKjY3VnDlzdODAAd1yyy06depUrv2TkpIUEBDg0BYQEKCkpKTLvk50dLR8fX3tS0hISJHtAwAAKH1KdQDq2rWr+vTpo4YNGyoyMlLffvutUlJS9Omnnxbp64wfP16pqan25ciRI0U6PwAAKF1cSrqAgqhQoYJuvPFGJSQk5Lo9MDBQycnJDm3JyckKDAy87Lzu7u5yd3cvsjoBAEDpVqqPAF3q9OnT2rdvn4KCgnLd3qpVK61cudKhbfny5WrVqtW1KA8AAJQRpToAjR49WmvWrNHBgwe1fv169erVS87Ozurfv78kaeDAgRo/fry9/2OPPabY2Fi98sor2rNnj5599llt3rxZI0aMKKldAAAApVCpPgV29OhR9e/fX8ePH5e/v7/atm2rjRs3yt/fX5J0+PBhOTn9L8O1bt1aH330kZ5++mk9+eSTqlWrlpYsWaL69euX1C4AAIBSyGaMMSVdRGmTlpYmX19fpaamysfHp+hfwGYr+jmB6wW/kgAUUkE+v0v1KTAAAIDiQAACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWU6oDUHR0tG6++WaVL19elStXVs+ePRUfH3/ZMTExMbLZbA6Lh4fHNaoYAACUBaU6AK1Zs0bDhw/Xxo0btXz5cp0/f15dunRRenr6Zcf5+PgoMTHRvhw6dOgaVQwAAMoCl5Iu4HJiY2Md1mNiYlS5cmVt2bJF//rXv/IcZ7PZFBgYWNzlAQCAMqpUHwG6VGpqqiSpYsWKl+13+vRphYaGKiQkRD169NDOnTsv2z8jI0NpaWkOCwAAuH6VmQCUnZ2tkSNHqk2bNqpfv36e/WrXrq333ntPX3zxhebPn6/s7Gy1bt1aR48ezXNMdHS0fH197UtISEhx7AIAACglbMYYU9JF5MfDDz+spUuX6scff1TVqlXzPe78+fOqW7eu+vfvrylTpuTaJyMjQxkZGfb1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\n" - }, - "metadata": {} - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Time Difference: 19.889 s\n" - ] - } + "data": { + "image/png": "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LEwxMTHy8/PT66+/rmnTpqlp06b66aefciyULg6vvvqqOnXqpOjoaL377rt6+OGHtXbtWnl6ekq6dkrt22+/Vf369RUdHa2oqChVq1ZNixYtuqnjPvXUU3J3d9f777+vyMhILVy4UL169dKKFSusASUgIECbN29Wu3btNGvWLI0ZM0YlSpTQt99+q0cfffSmXztQVPzWEgCruLg4NWjQQJ988gk3L7uN1q9fr7Zt2+qLL75Q9+7d7V0OYCrMyAB3qetvaS9JM2bMkJOTkx588EE7VAQAhccaGeAuNWXKFG3fvl1t27aVi4uL9bLbZ555Jsf9XADAURFkgLtU8+bNtXr1ak2YMEEXLlxQxYoV9cYbb9zRv/4M4M7DGhkAAGBarJEBAACmRZABAACmdcevkcnKytKff/6pkiVLOvRNvgAAwP8YhqHz588rKCgo3xsu3vFB5s8//+QKDAAATOr48eP5/or7HR9kSpYsKenaG+Hj42PnagAAQEGkpqYqODjY+j2elzs+yGSfTvLx8SHIAABgMjdaFsJiXwAAYFoEGQCAXXz00Udq3769vcu4LX777TdVqFBBaWlp9i7ljkOQAYC7TFJSkl588UWFhobK3d1dAQEBatGihebMmaOLFy9a+4WEhMhischiscjLy0sNGzbUF198Yd0eERGhbt265Rh//fr1slgsOnfuXJ41XL58WWPHjtW4ceOsbXv37tXjjz9uPe6MGTNy3Xf27NkKCQmRu7u7HnjgAW3ZsiXf1ztv3jy1atVKpUuXVunSpRUWFmazz5UrVzRq1CjVrVtXXl5eCgoKUt++ffXnn3/mOl56errq168vi8WiuLi4fI+drVatWmratKmmT59eoP4oOIIMANxFDh06pAYNGmjVqlV66623tHPnTsXGxuqVV17Rd999pzVr1tj0Hz9+vBITE7Vz507df//96tWrlzZv3nzTdXz55Zfy8fFRixYtrG0XL15UlSpVNGnSJAUGBua635IlSzR8+HCNGzdOO3bs0H333afw8HCdOnUqz2OtX79e//znP7Vu3TrFxsYqODhY7du314kTJ6zH3bFjh8aOHasdO3Zo6dKlio+PV5cuXXId75VXXlFQUFChX3P//v01Z84cXb16tdD7Ih/GHS4lJcWQZKSkpNi7FACwu/DwcKNChQrGhQsXct2elZVl/e9KlSoZ77zz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GDDA3qUBAAAH4NBBZtasWRo7dqwiIyN16tQpBQUF6dlnn9Xrr79u79IAAIADsBh3+G1yU1NT5evrq5SUFPn4+Ni7HAAmZImy2LsEwGEZ425NjCjo97dDr5EBAADID0EGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYFkEGAACYlktRd7xy5YqSkpJ08eJFlS1bVmXKlCnOugAAAG6oUDMy58+f15w5c9S6dWv5+PgoJCRENWvWVNmyZVWpUiUNGjRIW7duLdYCT5w4oaeeekp+fn7y8PBQ3bp1tW3btmI9BgAAMKcCB5np06crJCRE8+fPV1hYmL7++mvFxcXpwIEDio2N1bhx43T16lW1b99e//jHP3Tw4MGbLu7s2bNq0aKFXF1dtWLFCv322296++23Vbp06ZseGwAAmF+BTy1t3bpVGzZsUO3atXPd3qRJEw0YMEAffPCB5s+fr40bN6patWo3VdzkyZMVHBys+fPnW9sqV658U2MCAIA7h8UwDMPeReSlVq1aCg8P1x9//KEff/xR99xzjyIjIzVo0KA890lPT1d6err1eWpqqoKDg5WSkiIfH5/bUTaAO4wlymLvEgCHZYy7NTEiNTVVvr6+N/z+LparllJTU/X1119r3759xTGc1aFDhzRnzhxVq1ZNK1eu1PPPP6+hQ4dq4cKFee4THR0tX19f6yM4OLhYawIAAI6jSDMyPXv21IMPPqghQ4bo0qVLuu+++3TkyBEZhqHFixfr8ccfL5biSpQoocaNG2vz5s3WtqFDh2rr1q2KjY3NdR9m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kUdsjl25cmVJUoMGDWSxWNSmTRtJOWe80tPTNXToUJUrV07u7u5q2bKltm7dmuP9XLt2rRo3bixPT081b95c8fHxN/rjA/A3BBngLrVw4UL5+/try5YteuGFF/T888+rR48eat68uXbs2KH27durT58+unjxos1+I0eO1Ntvv62tW7eqbNmy6ty5s65cuZLrMTZu3Ki+ffvqxRdf1G+//aa5c+dqwYIFmjhxok2/CRMmqG/fvoqLi1ONGjX05JNP6tlnn9WYMWO0bds2GYahIUOGFHrcqKgo9ezZU7/++qs6duyo3r17Kzk5WcHBwfrqq68kSfHx8UpMTNS7774rSUpLS9Pw4cO1bds2rV27Vk5OTnr00UeVlZUlSdqyZYskac2aNUpMTNTSpUtzfe2vvPKKvvrqKy1cuFA7duxQaGiowsPDlZycbNPvtdde09tvv61t27bJxcVFAwYMyPfPDcB1DAB3hX79+hldu3Y1DMMwWrdubbRs2dK67erVq4aXl5fRp08fa1tiYqIhyYiNjTUMwzDWrVtnSDIWL15s7XPmzBnDw8PDWLJkiWEYhjF//nzD19fXuv3hhx823nrrLZs6/v3vfxvly5e3Ppdk/Otf/7I+j42NNSQZH330kbXt888/N9zd3W9q3AsXLhiSjBUrVti8nrNnz+bxjl3z119/GZKM3bt3G4ZhGIcPHzYkGTt37rTp9/f398KFC4arq6vx6aefWrdnZGQYQUFBxpQpU2yOv2bNGmuf5cuXG5KMS5cu5VsTgP9hjQxwl6pXr571v52dneXn56e6deta2wICAiRJp06dstmvWbNm1v8uU6aMqlevrn379uV6jF27dumnn36ymSnJzMzU5cuXdfHiRXl6euaoJfu419dy+fJlpaamysfHp0jjenl5ycfHJ8frud7Bgwf1+uuv65dfftHp06etMzHHjh1TnTp18t032++//64rV66oRYsW1jZXV1c1adIkx3v19xrLly8v6dp7XrFixQIdC7jbEWSAu5Srq6vNc4vFYtNmsVgkyfpFXhQXLlxQVFSUHnvssRzb3N3dc60l+7j51VKUcbPHudHr6dy5sypVqqR58+YpKChIWVlZqlOnjjIyMvLdr6iK+z0H7jYEGQCF8vPPP1tnC86ePasDBw6oZs2aufZt2LCh4uPjFRoaWqw1FMe4JUqUkHRtJifbmTNnFB8fr3nz5qlVq1aSpE2bNt1wv+tVrVpVJUqU0E8//aRKlSpJkq5cuaKtW7dq2LBhRa4ZQE4EGQCFMn78ePn5+SkgIECvvfaa/P3987w/zeuvv65HHnlEFStWVPfu3eXk5KRdu3Zpz549evPNN4tcQ3GMW6lSJVksFn333Xfq2LGjPDw8VLp0afn5+enDDz9U+fLldezYMY0ePdpmv3LlysnDw0MxMTGqUKGC3N3dc1x67eXlpeeff14jR45UmTJlVLFiRU2ZMkUXL17UwIEDi/y6AeTEVUsACmXSpEl68cUX1ahRIyUlJenbb7+1zlJcLzw8XN99951WrVql+++/X02bNtU777xjnaUoquIY95577lFUVJRGjx6tgIAADRkyRE5OTlq8eLG2b9+uOnXq6KWXXtLUqVNt9nNxcdHMmTM1d+5cBQUFqWvXrrmOP2nSJD3++OPq06ePGjZsqISEBK1cuVKlS5e+qdcOwJbFMAzD3kUAcHzr169X27Ztdfbs2bv2ZwgAOB5mZAAAgGkRZAAAgGlxagkAAJgWMzIAAMC0CDIAAMC0CDIAAMC0CDIAAMC0CDIAAMC0CDIAAMC0CDIAAMC0CDIAAMC0CDIAAMC0/h9VSYnwt21c4QAAAABJRU5ErkJggg==", + "text/plain": [ + "
" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "source": [], - "metadata": { - "id": "su7te99cs_Yi" - }, - "execution_count": null, - "outputs": [] + "name": "stdout", + "output_type": "stream", + "text": [ + "Time Difference: -5.573 s\n" + ] } - ] -} \ No newline at end of file + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "# ...\n", + "\n", + "with threadpool_limits(limits=N_THREADS, user_api='blas'):\n", + " # Time CPU-only implementation\n", + " start_time = time.time()\n", + " C_cpu = matmul_cpu_at_operator(A, B)\n", + " end_time = time.time()\n", + " cpu_time = end_time - start_time\n", + " print(\"CPU time: {:.3f} s\".format(cpu_time))\n", + "\n", + "\n", + "# Time PyCUDA-accelerated implementation\n", + "start_time = time.time()\n", + "C_gpu = matmul_gpu(A, B)\n", + "end_time = time.time()\n", + "gpu_time = end_time - start_time\n", + "print(\"GPU time: {:.3f} s\".format(gpu_time))\n", + "\n", + "# Calculate the time difference\n", + "time_diff = cpu_time - gpu_time\n", + "\n", + "# Plot the time difference\n", + "labels = ['CPU', 'GPU']\n", + "times = [cpu_time, gpu_time]\n", + "colors = ['red', 'green']\n", + "plt.bar(labels, times, color=colors)\n", + "plt.xlabel('Implementation')\n", + "plt.ylabel('Time (s)')\n", + "plt.title('Running Time Comparison')\n", + "\n", + "# Add time labels to the plot\n", + "for i in range(len(labels)):\n", + " plt.text(i, times[i], '{} ({:.3f}s)'.format(labels[i], times[i]), ha='center', va='bottom')\n", + "\n", + "plt.show()\n", + "\n", + "# Print the time difference\n", + "print(\"Time Difference: {:.3f} s\".format(time_diff))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +}