{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# GPU acceleration\n",
    "\n",
    "The package `torchtt` can use the built-in GPU acceleration from `pytorch`.\n",
    "\n",
    "Imports and check if any CUDA device is available."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CUDA available: True\n",
      "Device name: NVIDIA GeForce RTX 2080 Ti\n"
     ]
    }
   ],
   "source": [
    "import datetime\n",
    "import torch as tn\n",
    "try: \n",
    "    import torchtt as tntt\n",
    "except:\n",
    "    print('Installing torchTT...')\n",
    "    %pip install git+https://github.com/ion-g-ion/torchTT\n",
    "    import torchtt as tntt\n",
    "    \n",
    "print('CUDA available:',tn.cuda.is_available())\n",
    "print('Device name: ' + tn.cuda.get_device_name())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Define a function to test. It performs 2 matrix vector products in TT-format and a rank rounding.\n",
    "The return result is a scalar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def f(x,A,y):\n",
    "    \"\"\"\n",
    "    fonction that performs operations of tensors in TT.\n",
    "\n",
    "    Args:\n",
    "        x (tnt.TT): input TT tensor\n",
    "        A (tnt.TT): input TT matrix\n",
    "        y (tnt.TT): input TT tensor\n",
    "\n",
    "    Returns:\n",
    "        torch.tensor: result\n",
    "    \"\"\"\n",
    "    z = A @ y + A @ y # operatio that grows the rank\n",
    "    z = z.round(1e-12) # rank rounding (contains QR and SVD decomposition)\n",
    "    z += z+x # some other operation\n",
    "    return tntt.dot(x,z) # contract the tensor"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Generate random tensors in the TT-format (on the CPU)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x = tntt.random([200,300,400,500],[1,8,8,8,1])\n",
    "y = tntt.random([200,300,400,500],[1,8,8,8,1])\n",
    "A = tntt.random([(200,200),(300,300),(400,400),(500,500)],[1,8,8,8,1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Run the function `f()` and report the time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tme_cpu = datetime.datetime.now()\n",
    "f(x,A,y) \n",
    "tme_cpu = datetime.datetime.now() - tme_cpu\n",
    "print('Time on CPU: ',tme_cpu)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Move the defined tensors on GPU. Similarily to `pytorch` tensors one can use the function `cuda()` to return a copy of a TT instance on the GPU.\n",
    "All the cores of the returned TT object are on the GPU."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cuda_name = 'cuda:0'\n",
    "x = x.to(cuda_name)\n",
    "y = y.to(cuda_name)\n",
    "A = A.to(cuda_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function is executed once without timing to \"warm-up\" the CUDA."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f(x*0,A*0,0*y).cpu()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Run the function again. This time the runtime is reported. The return value is moved to CPU to assure blocking until all computations are done."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tme_gpu = datetime.datetime.now()\n",
    "f(x,A,y).cpu()\n",
    "tme_gpu = datetime.datetime.now() - tme_gpu\n",
    "print('Time with CUDA: ',tme_gpu)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The speedup is reported:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Speedup: ',tme_cpu.total_seconds()/tme_gpu.total_seconds(),' times.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This time we perform the same test without using the rank rounding. The expected result is better since the rank rounding contains QR and SVD which are not that parallelizable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def g(x,A,y):\n",
    "    \"\"\"\n",
    "    fonction that performs operations of tensors in TT.\n",
    "\n",
    "    Args:\n",
    "        x (tnt.TT): input TT tensor\n",
    "        A (tnt.TT): input TT matrix\n",
    "        y (tnt.TT): input TT tensor\n",
    "\n",
    "    Returns:\n",
    "        torch.tensor: result\n",
    "    \"\"\"\n",
    "    z = A @ y + A @ y # operatio that grows the rank\n",
    "    z += z+x # some other operation\n",
    "    return tntt.dot(x,z) # contract the tensor"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# put tensors on CPU\n",
    "x, y, A = x.cpu(), y.cpu(), A.cpu()\n",
    "# perform the test\n",
    "tme_cpu = datetime.datetime.now()\n",
    "g(x,A,y) \n",
    "tme_cpu = datetime.datetime.now() - tme_cpu\n",
    "print('Time on CPU: ',tme_cpu)\n",
    "# move the tensors back to GPU\n",
    "x, y, A = x.to(cuda_name), y.cuda(cuda_name), A.cuda(cuda_name)\n",
    "# execute the function\n",
    "tme_gpu = datetime.datetime.now()\n",
    "g(x,A,y).cpu()\n",
    "tme_gpu = datetime.datetime.now() - tme_gpu\n",
    "print('Time with CUDA: ',tme_gpu)\n",
    "\n",
    "print('Speedup: ',tme_cpu.total_seconds()/tme_gpu.total_seconds(),' times.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A tensor can be copied to a differenct device using the `torchtt.TT.to()` method. Usage is similar to `torch.tensor.to()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dev = tn.cuda.current_device()\n",
    "x_cuda = x.to(dev)\n",
    "x_cpu = x_cuda.to(None)"
   ]
  }
 ],
 "metadata": {
  "interpreter": {
   "hash": "df6fc3a9b7a9c6f4b0308ab6eb361a4cabbf6b5db181383d07014ff4304e5cb3"
  },
  "kernelspec": {
   "display_name": "Python 3.9.7 64-bit ('pytorch': conda)",
   "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.9.7"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
