{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Optimalizace až na cost\n", "\n", "Naučíme se optimalizovat funkce. Začneme od čisté implementace v Pythonu, zkusíme vyřešit problém v NumPy. Poté si ukážeme, jak postupně přejít k jazykům nižší úrovně, jako je Fortran nebo C, a také si představíme balík Numba.\n", "" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:17:08.363327Z", "start_time": "2024-04-18T08:17:08.072665Z" }, "collapsed": false }, "outputs": [], "source": [ "from IPython.display import Image\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Základní koncepce optimalizace\n", "\n", "Už víme, že Python je jazyk, ve kterém jde velice efektivně programovat. A díky balíkům, jako jsou NumPy, SciPy nebo SymPy jde velice rychle řešit různé vědecké úlohy. Je pochopitelné, že s takovouto mírou abstrakce může být výsledný program pomalejší, než kdyby byl dobře napsán v nějakém kompilovaném jazyce typu C/C++ nebo Fortran. Musíme si ovšem uvědomit, že efektivita programu se měří v *celkovém čase stráveném na vývoji a běhu programu* (pro daný soubor úloh). Schématicky můžeme znázornit závislost rychlosti běhu programu v závislosti na délce vývoje asi takto:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:17:10.193420Z", "start_time": "2024-04-18T08:17:10.188542Z" }, "collapsed": false }, "outputs": [ { "data": { "image/png": "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", "text/plain": [ "" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Image(filename='optimizing-what.png')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Všimněte si například, že typicky existuje nějaký offset ve vývojovém čase, tj. trvá nám déle v nízkoúrovňovém jazyce, než vůbec dostaneme první výsledek. Potřeba optimalizace tedy *silně závisí na objemu výpočtů, které budeme s daným programem řešit*.\n", "\n", "Toto ovšem *neznamená, že pokud je objem velký, máme hned začít programovat v C nebo Fortranu*. Za chvíli si ukážeme, jak optimalizaci řešit chytřeji a postupně. Empirické pravidlo říká, že 90 % výpočetního času zabere 10 % zdrojového kódu. Jedná se o konkrétní příklad obecného [Paretova 80 / 20 principu](https://cs.wikipedia.org/wiki/Paret%C5%AFv_princip). Je tedy vhodné nejprve těchto 10 % najít a poté je teprve začít optimalizovat. Python nám k tomuto účelu poskytuje velice mocné nástroje." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Profilování\n", "\n", "Profilování je nástroj, který nám umožní najít kritická místa v našem programu, oněch 10 %, které stojí za to optimalizovat. Zkusme si to ukázat na jednoduchém příkladu." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:17:12.089139Z", "start_time": "2024-04-18T08:17:12.084944Z" }, "collapsed": false }, "outputs": [], "source": [ "def heavy_calc(X):\n", " Y = X.copy()\n", " for i in range(10):\n", " Y = Y**i\n", " return Y\n", "\n", "def heavy_loop(inputs):\n", " res = []\n", " for X in inputs:\n", " res.append(heavy_calc(X))\n", " return res\n", "\n", "def code_setup():\n", " from numpy.random import rand\n", " N = 20\n", " M = 1000\n", " print(\"Will generate {} random arrays\".format(N))\n", " inputs = [rand(M, M) for n in range(N)]\n", " print(\"Will calculate now\")\n", " result = heavy_loop(inputs)\n", " print(\"Finished calculation\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Python obsahuje dva základní mofuly pro profilování - `profile` a `cProfile`, z nichž ten druhý je rychlejší. Pomocí funkce `run` pustíme výpočet pod dohledem cProfile, výsledky uložíme do souboru." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:17:16.723579Z", "start_time": "2024-04-18T08:17:15.663576Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Will generate 20 random arrays\n", "Will calculate now\n", "Finished calculation\n" ] } ], "source": [ "import cProfile\n", "cProfile.run('code_setup()', 'pstats')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Dále budeme potřebovat modul `pstats`, který nám umožní s výsledky pracovat. Použije k tomu třídu `Stats`." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:17:18.050402Z", "start_time": "2024-04-18T08:17:18.047259Z" }, "collapsed": false }, "outputs": [], "source": [ "from pstats import Stats\n", "p = Stats('pstats')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`print_stats` nám zobrazí prvních n záznamů." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:17:20.961915Z", "start_time": "2024-04-18T08:17:20.956328Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Sun Jan 19 13:23:06 2025 pstats\n", "\n", " 3892 function calls (3755 primitive calls) in 0.537 seconds\n", "\n", " Random listing order was used\n", " List reduced from 378 to 10 due to restriction <10>\n", "\n", " ncalls tottime percall cumtime percall filename:lineno(function)\n", " 13 0.000 0.000 0.000 0.000 /Users/kuba/workspace/fjfi/python-fjfi/.venv/lib/python3.12/site-packages/_distutils_hack/__init__.py:101(find_spec)\n", " 25 0.000 0.000 0.000 0.000 /Users/kuba/.local/share/uv/python/cpython-3.12.5-macos-aarch64-none/lib/python3.12/enum.py:1551(__or__)\n", " 17 0.000 0.000 0.000 0.000 /Users/kuba/.local/share/uv/python/cpython-3.12.5-macos-aarch64-none/lib/python3.12/enum.py:1562(__and__)\n", " 3 0.000 0.000 0.000 0.000 /Users/kuba/.local/share/uv/python/cpython-3.12.5-macos-aarch64-none/lib/python3.12/threading.py:1155(_wait_for_tstate_lock)\n", " 126 0.000 0.000 0.000 0.000 /Users/kuba/.local/share/uv/python/cpython-3.12.5-macos-aarch64-none/lib/python3.12/enum.py:1544(_get_value)\n", " 4 0.000 0.000 0.000 0.000 /Users/kuba/.local/share/uv/python/cpython-3.12.5-macos-aarch64-none/lib/python3.12/json/encoder.py:183(encode)\n", " 2 0.000 0.000 0.000 0.000 /Users/kuba/workspace/fjfi/python-fjfi/.venv/lib/python3.12/site-packages/_distutils_hack/__init__.py:108()\n", " 1 0.000 0.000 0.000 0.000 /Users/kuba/.local/share/uv/python/cpython-3.12.5-macos-aarch64-none/lib/python3.12/re/__init__.py:226(compile)\n", " 58 0.000 0.000 0.000 0.000 /Users/kuba/.local/share/uv/python/cpython-3.12.5-macos-aarch64-none/lib/python3.12/enum.py:726(__call__)\n", " 1 0.000 0.000 0.000 0.000 /Users/kuba/.local/share/uv/python/cpython-3.12.5-macos-aarch64-none/lib/python3.12/functools.py:35(update_wrapper)\n", "\n", "\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p.print_stats(10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ty jsou ovšem nesetříděné. Následující výstup už je užitečnější, záznamy jsou totiž setříděné podle celkového času stráveného v dané funkci. Navíc `strip_dirs` odstraní adresáře ze jmen funkcí." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:17:31.098424Z", "start_time": "2024-04-18T08:17:31.093655Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Sun Jan 19 13:23:06 2025 pstats\n", "\n", " 3892 function calls (3755 primitive calls) in 0.537 seconds\n", "\n", " Ordered by: cumulative time\n", " List reduced from 378 to 10 due to restriction <10>\n", "\n", " ncalls tottime percall cumtime percall filename:lineno(function)\n", " 8 0.079 0.010 0.762 0.095 base_events.py:1909(_run_once)\n", " 4/1 0.000 0.000 0.530 0.530 {built-in method builtins.exec}\n", " 20 0.416 0.021 0.431 0.022 2469674079.py:1(heavy_calc)\n", " 1 0.000 0.000 0.320 0.320 2469674079.py:13(code_setup)\n", " 1 0.000 0.000 0.320 0.320 2469674079.py:7(heavy_loop)\n", " 20 0.015 0.001 0.015 0.001 {method 'copy' of 'numpy.ndarray' objects}\n", " 13/1 0.000 0.000 0.014 0.014 :1349(_find_and_load)\n", " 13/1 0.000 0.000 0.014 0.014 :1304(_find_and_load_unlocked)\n", " 12/1 0.000 0.000 0.014 0.014 :911(_load_unlocked)\n", " 3/1 0.000 0.000 0.014 0.014 :989(exec_module)\n", "\n", "\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p.strip_dirs().sort_stats('cumulative').print_stats(10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Takto vypadá výstup setříděný pomocí nekumulovaného času." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "ExecuteTime": { "end_time": "2024-04-11T12:49:44.698688Z", "start_time": "2024-04-11T12:49:44.695290Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Sun Jan 19 13:23:06 2025 pstats\n", "\n", " 3892 function calls (3755 primitive calls) in 0.537 seconds\n", "\n", " Ordered by: internal time\n", " List reduced from 378 to 10 due to restriction <10>\n", "\n", " ncalls tottime percall cumtime percall filename:lineno(function)\n", " 20 0.416 0.021 0.431 0.022 2469674079.py:1(heavy_calc)\n", " 8 0.079 0.010 0.762 0.095 base_events.py:1909(_run_once)\n", " 20 0.015 0.001 0.015 0.001 {method 'copy' of 'numpy.ndarray' objects}\n", " 7/0 0.011 0.002 0.000 {method 'control' of 'select.kqueue' objects}\n", " 9 0.007 0.001 0.007 0.001 {built-in method _imp.create_dynamic}\n", " 9/6 0.002 0.000 0.006 0.001 {built-in method _imp.exec_dynamic}\n", " 35/2 0.001 0.000 0.012 0.006 :480(_call_with_frames_removed)\n", " 9/8 0.001 0.000 0.002 0.000 events.py:86(_run)\n", " 36 0.001 0.000 0.001 0.000 {built-in method posix.stat}\n", " 24 0.000 0.000 0.000 0.000 socket.py:626(send)\n", "\n", "\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p.sort_stats('time').print_stats(10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Jupyter nám může usnadnit práci pomocí `%prun` a `%%prun`. Např." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:21:57.247796Z", "start_time": "2024-04-18T08:21:55.778256Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Will generate 20 random arrays\n", "Will calculate now\n", "Finished calculation\n", " " ] }, { "name": "stdout", "output_type": "stream", "text": [ " 2184 function calls (2126 primitive calls) in 0.755 seconds\n", "\n", " Ordered by: cumulative time\n", " List reduced from 219 to 10 due to restriction <10>\n", "\n", " ncalls tottime percall cumtime percall filename:lineno(function)\n", " 1 0.000 0.000 0.589 0.589 {built-in method builtins.exec}\n", " 1 0.016 0.016 0.589 0.589 :1()\n", " 20 0.507 0.025 0.520 0.026 2469674079.py:1(heavy_calc)\n", " 14/13 0.009 0.001 0.375 0.029 base_events.py:1909(_run_once)\n", " 1 0.000 0.000 0.363 0.363 2469674079.py:13(code_setup)\n", " 1 0.000 0.000 0.363 0.363 2469674079.py:7(heavy_loop)\n", " 20 0.016 0.001 0.016 0.001 {method 'copy' of 'numpy.ndarray' objects}\n", " 15/13 0.000 0.000 0.003 0.000 {method 'run' of '_contextvars.Context' objects}\n", " 7 0.000 0.000 0.002 0.000 zmqstream.py:583(_handle_events)\n", " 5 0.000 0.000 0.002 0.000 asyncio.py:200(_handle_events)" ] } ], "source": [ "%prun -s cumulative -l 10 code_setup()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Z obou výstupů celkem jasně vydíme, že naprostou většinu času trávíme ve funkci `heavy_calc`. Pokud se tedy chceme pustit do optimalizace, musíme se zaměřit právě na tuto část našeho programu.\n", "\n", "Výsledky můžete navíc spojit s nástroji pro vizualizaci, např.[SnakeViz](http://jiffyclub.github.io/snakeviz/) nebo [vprof](https://github.com/nvdv/vprof), popř. pokročilý editor jako [PyCharm](https://www.jetbrains.com/pycharm/)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Vzorová úloha - vzdálenost množiny bodů ve vícerozměrném prostoru\n", "\n", "(Tento příklad byl převzat z http://jakevdp.github.io/blog/2013/06/15/numba-vs-cython-take-2.)\n", "\n", "Zadání je jednoduché: pro M bodů v N rozměrném prostoru spočítejte vzájemnou vzdálenost $d$, která je pro dva body $x,y$ definovaná jako $\\sqrt {\\sum_{i=1}^N {{{\\left( {{x_i} - {y_i}} \\right)}^2}} } $. Výslekem je tedy (symetrická) matice $M\\times M$." ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:22:00.402824Z", "start_time": "2024-04-18T08:22:00.399751Z" }, "collapsed": false }, "outputs": [], "source": [ "# toto nechť jsou naše vstupní data\n", "M = 1000\n", "N = 3\n", "X = np.random.random((M, N))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Implementace v čistém Pythonu\n", "Nemůžeme asi očekávat, že toto bude nejrychlejší a nejsnadnější verze našeho programu. Přesto stojí za to ji vyzkoušet, navíc ji budeme ještě potřebovat." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:22:01.347164Z", "start_time": "2024-04-18T08:22:01.342692Z" }, "collapsed": false }, "outputs": [], "source": [ "def pairwise_python(X):\n", " M = X.shape[0]\n", " N = X.shape[1]\n", " D = np.empty((M, M), dtype=float)\n", " for i in range(M):\n", " for j in range(M):\n", " d = 0.0\n", " for k in range(N):\n", " tmp = X[i, k] - X[j, k]\n", " d += tmp * tmp\n", " D[i, j] = np.sqrt(d)\n", " return D" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Tahle funkce nám bude pomáhat ukládat výsledné časy z `%timeit`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Do `pairwise_times` si uložíme výsledné časy." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:22:02.796241Z", "start_time": "2024-04-18T08:22:02.792442Z" }, "collapsed": false }, "outputs": [], "source": [ "pairwise_times = {}" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:22:15.299522Z", "start_time": "2024-04-18T08:22:03.284684Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.13 s ± 68.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "timings = %timeit -o pairwise_python(X)\n", "pairwise_times['plain_python'] = timings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### To samé pomocí NumPy\n", "V případě NumPy můžeme v tomto případě využít broadcasting. Celá funkce tak zabere jeden rádek." ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:22:15.303807Z", "start_time": "2024-04-18T08:22:15.300664Z" }, "collapsed": false }, "outputs": [], "source": [ "def pairwise_numpy(X):\n", " return np.sqrt(((X[:, np.newaxis, :] - X) ** 2).sum(-1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Zkusíme, jestli výsledky jsou stejné pomocí `assert` a `numpy.allclose`." ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:22:19.469565Z", "start_time": "2024-04-18T08:22:17.419668Z" } }, "outputs": [], "source": [ "assert np.allclose(pairwise_numpy(X), pairwise_python(X), rtol=1e-10, atol=1e-15)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Výsledky jsou stejné až na velmi malé rozdíly - to je nebezpečí numerických výpočtů s konečnou přesností." ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:22:23.531700Z", "start_time": "2024-04-18T08:22:21.409836Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "18.3 ms ± 221 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "timings = %timeit -o pairwise_numpy(X)\n", "pairwise_times['numpy'] = timings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vidíme, že jsme zkrátili běh programu více než 100-krát. To není špatné, navíc je implementace daleko jednodušší." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Přichází Cython\n", "\n", "[Cython](https://cython.org/) je nástroj, který z Python programu, obohaceného o nějaké Cython direktivy, vytvoří program v C (případně C++), který je možné zkompilovat a okamžitě použít jako modul v Pythonu. Typickým příkladem Cython direktiv jsou statické typy. Cython samozřejmě umožňuje používat funkce z binárních knihoven s C rozhraním.\n", "\n", "Zkusíme optimalizovat naší funkci `pairwise_python`.\n", "\n", "* Cython zdroják má koncovku .pyx (za začátku byl Pyrex).\n", "* Cython dokáže přeložit jakýkoli Python. Výsledkem je ale minimální (nebo spíš žádná) optimalizace.\n", "* `cimport` je analogie `import`, pracuje ale s Cython definicemi funkcí (.pxd soubory).\n", "* Cython dodává `numpy.pyx`, obsahující dodatečné informace pro kompilace NumPy modulů. Proto voláme `cimport numpy`.\n", "* Podobně `libc` je speciální modul Cythonu.\n", "\n", "* Funkce se deklarují (moho deklarovat) se statickými typy vstupních parametrů. My použijeme `np.ndarray[np.float64_t, ndim=2]`.\n", "* Proměnné se deklarují pomocí `cdef`.\n" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:22:30.642196Z", "start_time": "2024-04-18T08:22:30.639708Z" } }, "outputs": [], "source": [ "# Odkomentujte pro instalaci Cythonu\n", "# !pip install cython" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:22:32.130939Z", "start_time": "2024-04-18T08:22:32.127198Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting cyfuncs.pyx\n" ] } ], "source": [ "%%file cyfuncs.pyx\n", "\n", "language_level = \"3str\"\n", "\n", "import numpy as np\n", "# numpy pro Cython\n", "cimport numpy as np\n", "from libc.math cimport sqrt\n", "\n", "# tohle je čistý Python\n", "def pairwise0(X):\n", " M = X.shape[0]\n", " N = X.shape[1]\n", " D = np.empty((M, M), dtype=float)\n", " for i in range(M):\n", " for j in range(M):\n", " d = 0.0\n", " for k in range(N):\n", " tmp = X[i, k] - X[j, k]\n", " d += tmp * tmp\n", " D[i, j] = np.sqrt(d)\n", " return D\n", "\n", "# tady už začínáme optimalizovat, změny ale nejsou drastické\n", "def pairwise1(np.ndarray[np.float64_t, ndim=2] X):\n", " cdef int M = X.shape[0]\n", " cdef int N = X.shape[1]\n", " cdef double tmp, d\n", " cdef np.ndarray D = np.empty((M, M), dtype=np.float64)\n", " for i in range(M):\n", " for j in range(M):\n", " d = 0.0\n", " for k in range(N):\n", " tmp = X[i, k] - X[j, k]\n", " d += tmp * tmp\n", " D[i, j] = sqrt(d)\n", " return D" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:23:10.420625Z", "start_time": "2024-04-18T08:23:10.417392Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting setup.py\n" ] } ], "source": [ "%%file setup.py\n", "\n", "from distutils.core import setup\n", "from Cython.Build import cythonize\n", "import numpy\n", "\n", "setup(\n", " name='cyfuncs',\n", " include_dirs=[numpy.get_include()],\n", " ext_modules=cythonize(\"cyfuncs.pyx\"),\n", ")" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:23:13.073072Z", "start_time": "2024-04-18T08:23:10.984125Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Compiling cyfuncs.pyx because it changed.\n", "[1/1] Cythonizing cyfuncs.pyx\n", "/Users/kuba/workspace/fjfi/python-fjfi/.venv/lib/python3.12/site-packages/Cython/Compiler/Main.py:381: FutureWarning: Cython directive 'language_level' not set, using '3str' for now (Py3). This has changed from earlier releases! File: /Users/kuba/workspace/fjfi/python-fjfi/numerical_python_course/lecture_notes.cz/cyfuncs.pyx\n", " tree = Parsing.p_module(s, pxd, full_module_name)\n", "In file included from cyfuncs.c:1240:\n", "In file included from /Users/kuba/workspace/fjfi/python-fjfi/.venv/lib/python3.12/site-packages/numpy/_core/include/numpy/arrayobject.h:5:\n", "In file included from /Users/kuba/workspace/fjfi/python-fjfi/.venv/lib/python3.12/site-packages/numpy/_core/include/numpy/ndarrayobject.h:12:\n", "In file included from /Users/kuba/workspace/fjfi/python-fjfi/.venv/lib/python3.12/site-packages/numpy/_core/include/numpy/ndarraytypes.h:1909:\n", "\u001b[1m/Users/kuba/workspace/fjfi/python-fjfi/.venv/lib/python3.12/site-packages/numpy/_core/include/numpy/npy_1_7_deprecated_api.h:17:2: \u001b[0m\u001b[0;1;35mwarning: \u001b[0m\u001b[1m\"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-W#warnings]\u001b[0m\n", " 17 | #warning \"Using deprecated NumPy API, disable it with \" \\\u001b[0m\n", " | \u001b[0;1;32m ^\n", "\u001b[0m\u001b[1mcyfuncs.c:8531:26: \u001b[0m\u001b[0;1;35mwarning: \u001b[0m\u001b[1mcode will never be executed [-Wunreachable-code]\u001b[0m\n", " 8531 | module = PyImport_ImportModuleLevelObject(\u001b[0m\n", " | \u001b[0;1;32m ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", "\u001b[0m2 warnings generated.\n", "ld: warning: search path 'Modules/_hacl' not found\n", "ld: warning: search path '/install/lib' not found\n" ] } ], "source": [ "!python setup.py build_ext --inplace" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Jak jsme již říkali, Cython vytvoří C zdroják, který se pak kompiluje pomocí běžného překladače (např. gcc). Pojďme se na tento soubor podívat." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:23:17.343320Z", "start_time": "2024-04-18T08:23:17.339205Z" }, "collapsed": false }, "outputs": [ { "data": { "text/html": [ "cyfuncs.c
" ], "text/plain": [ "/Users/kuba/workspace/fjfi/python-fjfi/numerical_python_course/lecture_notes.cz/cyfuncs.c" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.display import FileLink\n", "FileLink('cyfuncs.c')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ten soubor je dlouhý ... Obsahuje spoustu Python \"balastu\", na kterém vidíme, jak je vlastně samotný CPython naprogramován. Naštěstí tento soubor obsahuje i komentáře, které říkají, které řádce daný blok odpovídá. Např.\n", "\n", " /* \"cyfuncs.pyx\":16\n", " * tmp = X[i, k] - X[j, k]\n", " * d += tmp * tmp\n", " * D[i, j] = np.sqrt(d) # <<<<<<<<<<<<<<\n", " * return D\n", " * \n", " */\n" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:23:19.619108Z", "start_time": "2024-04-18T08:23:19.615667Z" }, "collapsed": false }, "outputs": [], "source": [ "import cyfuncs" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:23:20.331403Z", "start_time": "2024-04-18T08:23:20.328054Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "cyfuncs obsahuje: language_level, np, pairwise0, pairwise1\n" ] } ], "source": [ "print(\"cyfuncs obsahuje: \" + \", \".join((d for d in dir(cyfuncs) if not d.startswith(\"_\"))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Podívejme se, jestli dostávám stále stejné výsledky." ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:23:28.253271Z", "start_time": "2024-04-18T08:23:28.160670Z" }, "collapsed": false }, "outputs": [], "source": [ "assert np.allclose(pairwise_numpy(X), cyfuncs.pairwise1(X), rtol=1e-10, atol=1e-15)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "No a jak jsme na tom s časem?" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:23:39.234005Z", "start_time": "2024-04-18T08:23:29.580814Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.13 s ± 41.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "timings = %timeit -o cyfuncs.pairwise0(X)\n", "pairwise_times['cython0'] = timings" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:23:44.674128Z", "start_time": "2024-04-18T08:23:39.235362Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "79.6 ms ± 3.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] } ], "source": [ "timings = %timeit -o cyfuncs.pairwise1(X)\n", "pairwise_times['cython1'] = timings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### IPython `%%cython` magic\n", "\n", "IPython, tak jako v mnoha jiných případech, nám práci s Cythonem usnadňuje pomocí triku `%%cython`. Zkusíme ho použít. Zároveň zkusíme ještě více náš kód optimalizovat, zatím je totiž pomalejší než numpy." ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:23:45.671385Z", "start_time": "2024-04-18T08:23:45.405330Z" }, "collapsed": false }, "outputs": [], "source": [ "%load_ext Cython" ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:23:49.316037Z", "start_time": "2024-04-18T08:23:45.830809Z" }, "collapsed": false }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_72b25b6e366aa2ac2fa9f6e8b1834b878e03df51.pyx\n", " \n", "\n", "\n", "

Generated by Cython 3.0.11

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
 01: 
\n", "
+02: import numpy as np
\n", "
  __pyx_t_7 = __Pyx_ImportDottedModule(__pyx_n_s_numpy, NULL); if (unlikely(!__pyx_t_7)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_7);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_np, __pyx_t_7) < 0) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_7); __pyx_t_7 = 0;\n",
       "/* … */\n",
       "  __pyx_t_7 = __Pyx_PyDict_NewPresized(0); if (unlikely(!__pyx_t_7)) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_7);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_7) < 0) __PYX_ERR(0, 2, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_7); __pyx_t_7 = 0;\n",
       "
 03: cimport numpy as np
\n", "
 04: cimport cython
\n", "
 05: from libc.math cimport sqrt
\n", "
 06: 
\n", "
+07: @cython.boundscheck(False)
\n", "
/* Python wrapper */\n",
       "static PyObject *__pyx_pw_54_cython_magic_72b25b6e366aa2ac2fa9f6e8b1834b878e03df51_1pairwise_cython(PyObject *__pyx_self, \n",
       "#if CYTHON_METH_FASTCALL\n",
       "PyObject *const *__pyx_args, Py_ssize_t __pyx_nargs, PyObject *__pyx_kwds\n",
       "#else\n",
       "PyObject *__pyx_args, PyObject *__pyx_kwds\n",
       "#endif\n",
       "); /*proto*/\n",
       "static PyMethodDef __pyx_mdef_54_cython_magic_72b25b6e366aa2ac2fa9f6e8b1834b878e03df51_1pairwise_cython = {\"pairwise_cython\", (PyCFunction)(void*)(__Pyx_PyCFunction_FastCallWithKeywords)__pyx_pw_54_cython_magic_72b25b6e366aa2ac2fa9f6e8b1834b878e03df51_1pairwise_cython, __Pyx_METH_FASTCALL|METH_KEYWORDS, 0};\n",
       "static PyObject *__pyx_pw_54_cython_magic_72b25b6e366aa2ac2fa9f6e8b1834b878e03df51_1pairwise_cython(PyObject *__pyx_self, \n",
       "#if CYTHON_METH_FASTCALL\n",
       "PyObject *const *__pyx_args, Py_ssize_t __pyx_nargs, PyObject *__pyx_kwds\n",
       "#else\n",
       "PyObject *__pyx_args, PyObject *__pyx_kwds\n",
       "#endif\n",
       ") {\n",
       "  __Pyx_memviewslice __pyx_v_X = { 0, 0, { 0 }, { 0 }, { 0 } };\n",
       "  #if !CYTHON_METH_FASTCALL\n",
       "  CYTHON_UNUSED Py_ssize_t __pyx_nargs;\n",
       "  #endif\n",
       "  CYTHON_UNUSED PyObject *const *__pyx_kwvalues;\n",
       "  PyObject *__pyx_r = 0;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"pairwise_cython (wrapper)\", 0);\n",
       "  #if !CYTHON_METH_FASTCALL\n",
       "  #if CYTHON_ASSUME_SAFE_MACROS\n",
       "  __pyx_nargs = PyTuple_GET_SIZE(__pyx_args);\n",
       "  #else\n",
       "  __pyx_nargs = PyTuple_Size(__pyx_args); if (unlikely(__pyx_nargs < 0)) return NULL;\n",
       "  #endif\n",
       "  #endif\n",
       "  __pyx_kwvalues = __Pyx_KwValues_FASTCALL(__pyx_args, __pyx_nargs);\n",
       "  {\n",
       "    PyObject **__pyx_pyargnames[] = {&__pyx_n_s_X,0};\n",
       "  PyObject* values[1] = {0};\n",
       "    if (__pyx_kwds) {\n",
       "      Py_ssize_t kw_args;\n",
       "      switch (__pyx_nargs) {\n",
       "        case  1: values[0] = __Pyx_Arg_FASTCALL(__pyx_args, 0);\n",
       "        CYTHON_FALLTHROUGH;\n",
       "        case  0: break;\n",
       "        default: goto __pyx_L5_argtuple_error;\n",
       "      }\n",
       "      kw_args = __Pyx_NumKwargs_FASTCALL(__pyx_kwds);\n",
       "      switch (__pyx_nargs) {\n",
       "        case  0:\n",
       "        if (likely((values[0] = __Pyx_GetKwValue_FASTCALL(__pyx_kwds, __pyx_kwvalues, __pyx_n_s_X)) != 0)) {\n",
       "          (void)__Pyx_Arg_NewRef_FASTCALL(values[0]);\n",
       "          kw_args--;\n",
       "        }\n",
       "        else if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 7, __pyx_L3_error)\n",
       "        else goto __pyx_L5_argtuple_error;\n",
       "      }\n",
       "      if (unlikely(kw_args > 0)) {\n",
       "        const Py_ssize_t kwd_pos_args = __pyx_nargs;\n",
       "        if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_kwvalues, __pyx_pyargnames, 0, values + 0, kwd_pos_args, \"pairwise_cython\") < 0)) __PYX_ERR(0, 7, __pyx_L3_error)\n",
       "      }\n",
       "    } else if (unlikely(__pyx_nargs != 1)) {\n",
       "      goto __pyx_L5_argtuple_error;\n",
       "    } else {\n",
       "      values[0] = __Pyx_Arg_FASTCALL(__pyx_args, 0);\n",
       "    }\n",
       "    __pyx_v_X = __Pyx_PyObject_to_MemoryviewSlice_d_dc_double(values[0], PyBUF_WRITABLE); if (unlikely(!__pyx_v_X.memview)) __PYX_ERR(0, 9, __pyx_L3_error)\n",
       "  }\n",
       "  goto __pyx_L6_skip;\n",
       "  __pyx_L5_argtuple_error:;\n",
       "  __Pyx_RaiseArgtupleInvalid(\"pairwise_cython\", 1, 1, 1, __pyx_nargs); __PYX_ERR(0, 7, __pyx_L3_error)\n",
       "  __pyx_L6_skip:;\n",
       "  goto __pyx_L4_argument_unpacking_done;\n",
       "  __pyx_L3_error:;\n",
       "  {\n",
       "    Py_ssize_t __pyx_temp;\n",
       "    for (__pyx_temp=0; __pyx_temp < (Py_ssize_t)(sizeof(values)/sizeof(values[0])); ++__pyx_temp) {\n",
       "      __Pyx_Arg_XDECREF_FASTCALL(values[__pyx_temp]);\n",
       "    }\n",
       "  }\n",
       "  __PYX_XCLEAR_MEMVIEW(&__pyx_v_X, 1);\n",
       "  __Pyx_AddTraceback(\"_cython_magic_72b25b6e366aa2ac2fa9f6e8b1834b878e03df51.pairwise_cython\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return NULL;\n",
       "  __pyx_L4_argument_unpacking_done:;\n",
       "  __pyx_r = __pyx_pf_54_cython_magic_72b25b6e366aa2ac2fa9f6e8b1834b878e03df51_pairwise_cython(__pyx_self, __pyx_v_X);\n",
       "  int __pyx_lineno = 0;\n",
       "  const char *__pyx_filename = NULL;\n",
       "  int __pyx_clineno = 0;\n",
       "\n",
       "  /* function exit code */\n",
       "  __PYX_XCLEAR_MEMVIEW(&__pyx_v_X, 1);\n",
       "  {\n",
       "    Py_ssize_t __pyx_temp;\n",
       "    for (__pyx_temp=0; __pyx_temp < (Py_ssize_t)(sizeof(values)/sizeof(values[0])); ++__pyx_temp) {\n",
       "      __Pyx_Arg_XDECREF_FASTCALL(values[__pyx_temp]);\n",
       "    }\n",
       "  }\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "static PyObject *__pyx_pf_54_cython_magic_72b25b6e366aa2ac2fa9f6e8b1834b878e03df51_pairwise_cython(CYTHON_UNUSED PyObject *__pyx_self, __Pyx_memviewslice __pyx_v_X) {\n",
       "  int __pyx_v_M;\n",
       "  int __pyx_v_N;\n",
       "  double __pyx_v_tmp;\n",
       "  double __pyx_v_d;\n",
       "  __Pyx_memviewslice __pyx_v_D = { 0, 0, { 0 }, { 0 }, { 0 } };\n",
       "  int __pyx_v_i;\n",
       "  int __pyx_v_j;\n",
       "  int __pyx_v_k;\n",
       "  PyObject *__pyx_r = NULL;\n",
       "/* … */\n",
       "  /* function exit code */\n",
       "  __pyx_L1_error:;\n",
       "  __Pyx_XDECREF(__pyx_t_1);\n",
       "  __Pyx_XDECREF(__pyx_t_2);\n",
       "  __Pyx_XDECREF(__pyx_t_3);\n",
       "  __Pyx_XDECREF(__pyx_t_4);\n",
       "  __Pyx_XDECREF(__pyx_t_5);\n",
       "  __PYX_XCLEAR_MEMVIEW(&__pyx_t_6, 1);\n",
       "  __Pyx_AddTraceback(\"_cython_magic_72b25b6e366aa2ac2fa9f6e8b1834b878e03df51.pairwise_cython\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __pyx_r = NULL;\n",
       "  __pyx_L0:;\n",
       "  __PYX_XCLEAR_MEMVIEW(&__pyx_v_D, 1);\n",
       "  __Pyx_XGIVEREF(__pyx_r);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "/* … */\n",
       "  __pyx_tuple__22 = PyTuple_Pack(9, __pyx_n_s_X, __pyx_n_s_M, __pyx_n_s_N, __pyx_n_s_tmp, __pyx_n_s_d, __pyx_n_s_D, __pyx_n_s_i, __pyx_n_s_j, __pyx_n_s_k); if (unlikely(!__pyx_tuple__22)) __PYX_ERR(0, 7, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_tuple__22);\n",
       "  __Pyx_GIVEREF(__pyx_tuple__22);\n",
       "/* … */\n",
       "  __pyx_t_7 = __Pyx_CyFunction_New(&__pyx_mdef_54_cython_magic_72b25b6e366aa2ac2fa9f6e8b1834b878e03df51_1pairwise_cython, 0, __pyx_n_s_pairwise_cython, NULL, __pyx_n_s_cython_magic_72b25b6e366aa2ac2f, __pyx_d, ((PyObject *)__pyx_codeobj__23)); if (unlikely(!__pyx_t_7)) __PYX_ERR(0, 7, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_7);\n",
       "  if (PyDict_SetItem(__pyx_d, __pyx_n_s_pairwise_cython, __pyx_t_7) < 0) __PYX_ERR(0, 7, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_7); __pyx_t_7 = 0;\n",
       "
 08: @cython.wraparound(False)
\n", "
 09: def pairwise_cython(double[:, ::1] X):
\n", "
+10:     cdef int M = X.shape[0]
\n", "
  __pyx_v_M = (__pyx_v_X.shape[0]);\n",
       "
+11:     cdef int N = X.shape[1]
\n", "
  __pyx_v_N = (__pyx_v_X.shape[1]);\n",
       "
 12:     cdef double tmp, d
\n", "
+13:     cdef double[:, ::1] D = np.empty((M, M), dtype=np.float64)
\n", "
  __Pyx_GetModuleGlobalName(__pyx_t_1, __pyx_n_s_np); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __pyx_t_2 = __Pyx_PyObject_GetAttrStr(__pyx_t_1, __pyx_n_s_empty); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_2);\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  __pyx_t_1 = __Pyx_PyInt_From_int(__pyx_v_M); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __pyx_t_3 = __Pyx_PyInt_From_int(__pyx_v_M); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_3);\n",
       "  __pyx_t_4 = PyTuple_New(2); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __Pyx_GIVEREF(__pyx_t_1);\n",
       "  if (__Pyx_PyTuple_SET_ITEM(__pyx_t_4, 0, __pyx_t_1)) __PYX_ERR(0, 13, __pyx_L1_error);\n",
       "  __Pyx_GIVEREF(__pyx_t_3);\n",
       "  if (__Pyx_PyTuple_SET_ITEM(__pyx_t_4, 1, __pyx_t_3)) __PYX_ERR(0, 13, __pyx_L1_error);\n",
       "  __pyx_t_1 = 0;\n",
       "  __pyx_t_3 = 0;\n",
       "  __pyx_t_3 = PyTuple_New(1); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_3);\n",
       "  __Pyx_GIVEREF(__pyx_t_4);\n",
       "  if (__Pyx_PyTuple_SET_ITEM(__pyx_t_3, 0, __pyx_t_4)) __PYX_ERR(0, 13, __pyx_L1_error);\n",
       "  __pyx_t_4 = 0;\n",
       "  __pyx_t_4 = __Pyx_PyDict_NewPresized(1); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __Pyx_GetModuleGlobalName(__pyx_t_1, __pyx_n_s_np); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __pyx_t_5 = __Pyx_PyObject_GetAttrStr(__pyx_t_1, __pyx_n_s_float64); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;\n",
       "  if (PyDict_SetItem(__pyx_t_4, __pyx_n_s_dtype, __pyx_t_5) < 0) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;\n",
       "  __pyx_t_5 = __Pyx_PyObject_Call(__pyx_t_2, __pyx_t_3, __pyx_t_4); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_5);\n",
       "  __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;\n",
       "  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "  __pyx_t_6 = __Pyx_PyObject_to_MemoryviewSlice_d_dc_double(__pyx_t_5, PyBUF_WRITABLE); if (unlikely(!__pyx_t_6.memview)) __PYX_ERR(0, 13, __pyx_L1_error)\n",
       "  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;\n",
       "  __pyx_v_D = __pyx_t_6;\n",
       "  __pyx_t_6.memview = NULL;\n",
       "  __pyx_t_6.data = NULL;\n",
       "
+14:     for i in range(M):
\n", "
  __pyx_t_7 = __pyx_v_M;\n",
       "  __pyx_t_8 = __pyx_t_7;\n",
       "  for (__pyx_t_9 = 0; __pyx_t_9 < __pyx_t_8; __pyx_t_9+=1) {\n",
       "    __pyx_v_i = __pyx_t_9;\n",
       "
+15:         for j in range(M):
\n", "
    __pyx_t_10 = __pyx_v_M;\n",
       "    __pyx_t_11 = __pyx_t_10;\n",
       "    for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n",
       "      __pyx_v_j = __pyx_t_12;\n",
       "
+16:             d = 0.0
\n", "
      __pyx_v_d = 0.0;\n",
       "
+17:             for k in range(N):
\n", "
      __pyx_t_13 = __pyx_v_N;\n",
       "      __pyx_t_14 = __pyx_t_13;\n",
       "      for (__pyx_t_15 = 0; __pyx_t_15 < __pyx_t_14; __pyx_t_15+=1) {\n",
       "        __pyx_v_k = __pyx_t_15;\n",
       "
+18:                 tmp = X[i, k] - X[j, k]
\n", "
        __pyx_t_16 = __pyx_v_i;\n",
       "        __pyx_t_17 = __pyx_v_k;\n",
       "        __pyx_t_18 = __pyx_v_j;\n",
       "        __pyx_t_19 = __pyx_v_k;\n",
       "        __pyx_v_tmp = ((*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_X.data + __pyx_t_16 * __pyx_v_X.strides[0]) )) + __pyx_t_17)) ))) - (*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_X.data + __pyx_t_18 * __pyx_v_X.strides[0]) )) + __pyx_t_19)) ))));\n",
       "
+19:                 d += tmp * tmp
\n", "
        __pyx_v_d = (__pyx_v_d + (__pyx_v_tmp * __pyx_v_tmp));\n",
       "      }\n",
       "
+20:             D[i, j] = sqrt(d)
\n", "
      __pyx_t_19 = __pyx_v_i;\n",
       "      __pyx_t_18 = __pyx_v_j;\n",
       "      *((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_D.data + __pyx_t_19 * __pyx_v_D.strides[0]) )) + __pyx_t_18)) )) = sqrt(__pyx_v_d);\n",
       "    }\n",
       "  }\n",
       "
+21:     return np.asarray(D)
\n", "
  __Pyx_XDECREF(__pyx_r);\n",
       "  __Pyx_GetModuleGlobalName(__pyx_t_4, __pyx_n_s_np); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 21, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __pyx_t_3 = __Pyx_PyObject_GetAttrStr(__pyx_t_4, __pyx_n_s_asarray); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 21, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_3);\n",
       "  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "  __pyx_t_4 = __pyx_memoryview_fromslice(__pyx_v_D, 2, (PyObject *(*)(char *)) __pyx_memview_get_double, (int (*)(char *, PyObject *)) __pyx_memview_set_double, 0);; if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 21, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_4);\n",
       "  __pyx_t_2 = NULL;\n",
       "  __pyx_t_20 = 0;\n",
       "  #if CYTHON_UNPACK_METHODS\n",
       "  if (unlikely(PyMethod_Check(__pyx_t_3))) {\n",
       "    __pyx_t_2 = PyMethod_GET_SELF(__pyx_t_3);\n",
       "    if (likely(__pyx_t_2)) {\n",
       "      PyObject* function = PyMethod_GET_FUNCTION(__pyx_t_3);\n",
       "      __Pyx_INCREF(__pyx_t_2);\n",
       "      __Pyx_INCREF(function);\n",
       "      __Pyx_DECREF_SET(__pyx_t_3, function);\n",
       "      __pyx_t_20 = 1;\n",
       "    }\n",
       "  }\n",
       "  #endif\n",
       "  {\n",
       "    PyObject *__pyx_callargs[2] = {__pyx_t_2, __pyx_t_4};\n",
       "    __pyx_t_5 = __Pyx_PyObject_FastCall(__pyx_t_3, __pyx_callargs+1-__pyx_t_20, 1+__pyx_t_20);\n",
       "    __Pyx_XDECREF(__pyx_t_2); __pyx_t_2 = 0;\n",
       "    __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;\n",
       "    if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 21, __pyx_L1_error)\n",
       "    __Pyx_GOTREF(__pyx_t_5);\n",
       "    __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;\n",
       "  }\n",
       "  __pyx_r = __pyx_t_5;\n",
       "  __pyx_t_5 = 0;\n",
       "  goto __pyx_L0;\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a\n", "\n", "import numpy as np\n", "cimport numpy as np\n", "cimport cython\n", "from libc.math cimport sqrt\n", "\n", "@cython.boundscheck(False)\n", "@cython.wraparound(False)\n", "def pairwise_cython(double[:, ::1] X):\n", " cdef int M = X.shape[0]\n", " cdef int N = X.shape[1]\n", " cdef double tmp, d\n", " cdef double[:, ::1] D = np.empty((M, M), dtype=np.float64)\n", " for i in range(M):\n", " for j in range(M):\n", " d = 0.0\n", " for k in range(N):\n", " tmp = X[i, k] - X[j, k]\n", " d += tmp * tmp\n", " D[i, j] = sqrt(d)\n", " return np.asarray(D)" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:24:09.786064Z", "start_time": "2024-04-18T08:24:09.723085Z" }, "collapsed": false }, "outputs": [], "source": [ "assert np.allclose(pairwise_numpy(X), pairwise_cython(X), rtol=1e-10, atol=1e-15)" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:24:23.467435Z", "start_time": "2024-04-18T08:24:10.196820Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.98 ms ± 40.3 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "timings = %timeit -o pairwise_cython(X)\n", "pairwise_times['cython2'] = timings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Tohle je už konečně výrazné zrychlení oproti NumPy." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Cython toho nabízí mnoho\n", "Podívejte se na http://docs.cython.org co všechno Cython nabízí -- není toho málo, např.\n", "\n", "* použití C++\n", "* šablony (templates)\n", "* OpenMP (k tomu se možná ještě dostaneme)\n", "* vytváření C-API\n", "* třídy (cdef classes)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Z Fortranu (nebo C) do Pythonu pomocí F2PY\n", "\n", "F2PY je nástroj, který byl v podstatě vytvořen pro NumPy a SciPy, protože, jak dobře víme, tyto moduly volají externí knihovny napsané ve Fortrane nebo C. Dokumentaci (trochu zastaralou) najdeme [zde](http://cens.ioc.ee/projects/f2py2e/usersguide/index.html). Bylo tedy velice výhodné vytvořit nástroj, který toto usnadní. A tak se zrodilo F2PY. Ve zkratce, F2PY umožňuje velice jednoduše z Fortran nebo C funkcí vytvořit Python modul. Využívá navíc vlastností NumPy pro předávání vícerozměnrých polí. \n", "\n", "Poďme chvilku programovat ve Fortranu :)" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:25:02.527832Z", "start_time": "2024-04-18T08:25:02.523778Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting pairwise_fort.f90\n" ] } ], "source": [ "%%file pairwise_fort.f90\n", "\n", "subroutine pairwise_fort(X, D, m, n)\n", " integer :: n,m\n", " double precision, intent(in) :: X(m, n)\n", " double precision, intent(out) :: D(m, m)\n", " integer :: i, j, k\n", " double precision :: r\n", "\n", " do j = 1,m\n", " do i = 1,m\n", " r = 0\n", " do k = 1,n\n", " r = r + (X(i,k) - X(j,k)) * (X(i,k) - X(j,k))\n", " end do\n", " D(i,j) = sqrt(r)\n", " end do\n", " end do\n", "end subroutine pairwise_fort" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Z čeho f2py bere informace o vytvoření modulu?\n", "\n", "1. Pole (double precision) převádí na numpy array.\n", "2. `intent(in)` = vstupní argument.\n", "3. `intent(out)` = výstupní argument.\n", "4. f2py schová explicitně zadané rozměry polí (m, n).\n", "\n", "Pokud bychom programovali v C, je potřeba dodat f2py nějaké informace navíc, neboť např. intent v C neexistuje. \n", "\n", "Tento soubor přeložíme pomocí `f2py`:" ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:25:05.699853Z", "start_time": "2024-04-18T08:25:04.807425Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cannot use distutils backend with Python>=3.12, using meson backend instead.\n", "Using meson backend\n", "Will pass --lower to f2py\n", "See https://numpy.org/doc/stable/f2py/buildtools/meson.html\n", "Reading fortran codes...\n", "\tReading file 'pairwise_fort.f90' (format:free)\n", "Post-processing...\n", "\tBlock: pairwise_fort\n", "\t\t\tBlock: pairwise_fort\n", "Applying post-processing hooks...\n", " character_backward_compatibility_hook\n", "Post-processing (stage 2)...\n", "Building modules...\n", " Building module \"pairwise_fort\"...\n", " Generating possibly empty wrappers\"\n", " Maybe empty \"pairwise_fort-f2pywrappers.f\"\n", " Constructing wrapper function \"pairwise_fort\"...\n", " d = pairwise_fort(x,[m,n])\n", " Wrote C/API module \"pairwise_fort\" to file \"./pairwise_fortmodule.c\"\n", "\u001b[1mThe Meson build system\u001b[0m\n", "Version: 1.6.1\n", "Source dir: \u001b[1m/private/var/folders/dm/gbbql3p121z0tr22r2z98vy00000gn/T/tmp5ugidbi4\u001b[0m\n", "Build dir: \u001b[1m/private/var/folders/dm/gbbql3p121z0tr22r2z98vy00000gn/T/tmp5ugidbi4/bbdir\u001b[0m\n", "Build type: \u001b[1mnative build\u001b[0m\n", "Project name: \u001b[1mpairwise_fort\u001b[0m\n", "Project version: \u001b[1m0.1\u001b[0m\n", "Fortran compiler for the host machine: \u001b[1mgfortran\u001b[0m (gcc 14.2.0 \"GNU Fortran (Homebrew GCC 14.2.0_1) 14.2.0\")\n", "Fortran linker for the host machine: \u001b[1mgfortran\u001b[0m ld64 1115.7.3\n", "C compiler for the host machine: \u001b[1mcc\u001b[0m (clang 16.0.0 \"Apple clang version 16.0.0 (clang-1600.0.26.6)\")\n", "C linker for the host machine: \u001b[1mcc\u001b[0m ld64 1115.7.3\n", "Host machine cpu family: \u001b[1maarch64\u001b[0m\n", "Host machine cpu: \u001b[1maarch64\u001b[0m\n", "Program /Users/kuba/workspace/fjfi/python-fjfi/.venv/bin/python3 found: \u001b[1;32mYES\u001b[0m (/Users/kuba/workspace/fjfi/python-fjfi/.venv/bin/python3)\n", "Found pkg-config: \u001b[1;32mYES\u001b[0m \u001b[1m(/opt/homebrew/bin/pkg-config)\u001b[0m \u001b[1;34m2.3.0\u001b[0m\n", "Run-time dependency \u001b[1mpython\u001b[0m found: \u001b[1;32mYES\u001b[0m \u001b[36m3.12\u001b[0m\n", "Library \u001b[1mquadmath\u001b[0m found: \u001b[1;32mYES\u001b[0m\n", "Build targets in project: \u001b[1m1\u001b[0m\n", "\n", "Found ninja-1.11.1.git.kitware.jobserver-1 at /Users/kuba/workspace/fjfi/python-fjfi/.venv/bin/ninja\n", "\u001b[1;32mINFO:\u001b[0m autodetecting backend as ninja\n", "\u001b[1;32mINFO:\u001b[0m calculating backend command to run: /Users/kuba/workspace/fjfi/python-fjfi/.venv/bin/ninja -C /private/var/folders/dm/gbbql3p121z0tr22r2z98vy00000gn/T/tmp5ugidbi4/bbdir\n", "ninja: Entering directory `/private/var/folders/dm/gbbql3p121z0tr22r2z98vy00000gn/T/tmp5ugidbi4/bbdir'\n", "[3/6] Compiling Fortran object pairwis...in.so.p/pairwise_fort-f2pywrappers.f.o\u001b[K\n", "\u001b[01m\u001b[Kf951:\u001b[m\u001b[K \u001b[01;35m\u001b[KWarning:\u001b[m\u001b[K Nonexistent include directory '\u001b[01m\u001b[K/install/include/python3.12\u001b[m\u001b[K' [\u001b[01;35m\u001b[K-Wmissing-include-dirs\u001b[m\u001b[K]\n", "[4/6] Compiling Fortran object pairwis...on-312-darwin.so.p/pairwise_fort.f90.o\u001b[K\n", "\u001b[01m\u001b[Kf951:\u001b[m\u001b[K \u001b[01;35m\u001b[KWarning:\u001b[m\u001b[K Nonexistent include directory '\u001b[01m\u001b[K/install/include/python3.12\u001b[m\u001b[K' [\u001b[01;35m\u001b[K-Wmissing-include-dirs\u001b[m\u001b[K]\n", "[6/6] Linking target pairwise_fort.cpython-312-darwin.so\u001b[Krc_fortranobject.c.o\u001b[K\n" ] } ], "source": [ "!f2py -c pairwise_fort.f90 -m pairwise_fort" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`-m pairwise_fort` je požadované jméno modulu. Můžeme ho rovnou importovat, resp. jeho stejnojmennou funkci." ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:25:11.219983Z", "start_time": "2024-04-18T08:25:11.216408Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "d = pairwise_fort(x,[m,n])\n", "\n", "Wrapper for ``pairwise_fort``.\n", "\n", "Parameters\n", "----------\n", "x : input rank-2 array('d') with bounds (m,n)\n", "\n", "Other Parameters\n", "----------------\n", "m : input int, optional\n", " Default: shape(x, 0)\n", "n : input int, optional\n", " Default: shape(x, 1)\n", "\n", "Returns\n", "-------\n", "d : rank-2 array('d') with bounds (m,m)\n", "\n" ] } ], "source": [ "from pairwise_fort import pairwise_fort\n", "print(pairwise_fort.__doc__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Fortran a C používají jiné uspořádání paměti pro ukládání vícerozměrných polí. Fortran je \"column-major\" zatímco C je \"row-major\". NumPy dokáže pracovat s obojím a pro uživatele je to obvykle jedno. Pokud ovšem chceme předat vícerozměrné pole do Fortran funkce, je lepší mít prvky uložené v paměti jako to dělá Fortran. V takovém případě totiž f2py předá pouze referenci (ukazatel) na dané místo v paměti. V opačném případě f2py nejprve pole musí transponovat, tj. *vytvořit kopii* s jiným uspořádáním, což může být samozřejmě náročné na pamět a procesor.\n", "\n", "Vytvoříme si proměnnou XF, která má Fortran uspořádání, pomocí `numpy.asfortranarray` (prozaický název :)" ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:25:15.329108Z", "start_time": "2024-04-18T08:25:15.326631Z" }, "collapsed": false }, "outputs": [], "source": [ "XF = np.asfortranarray(X)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vyzkoušíme, jestli stále dostáváme stejné výsledky." ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:25:17.031710Z", "start_time": "2024-04-18T08:25:16.997015Z" }, "collapsed": false }, "outputs": [], "source": [ "assert np.allclose(pairwise_numpy(X), pairwise_fort(X), rtol=1e-10, atol=1e-15)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "No a konečně se můžeme podívat, jak je to s rychlostí ..." ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:25:34.844746Z", "start_time": "2024-04-18T08:25:20.063009Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2.43 ms ± 251 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "timings = %timeit -o pairwise_fort(X)\n", "pairwise_times['fortran'] = timings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Představuje se `numba`\n", "\n", "Numba kompiluje Python kód pomocí [LLVM](http://llvm.org/). Podporujme just-in-time kompilaci pomocí dekorátoru `jit` (http://numba.pydata.org/numba-doc/latest/user/jit.html)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```\n", "@numba.jit(\n", " signature=None, \n", " nopython=False, \n", " nogil=False, \n", " cache=False, \n", " forceobj=False, \n", " parallel=False, \n", " error_model='python', \n", " fastmath=False, locals={}\n", ")\n", "```" ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:25:41.072952Z", "start_time": "2024-04-18T08:25:41.070431Z" } }, "outputs": [], "source": [ "# Odkomentujte pro instalaci balíku Numba\n", "# !pip install numba" ] }, { "cell_type": "code", "execution_count": 38, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:25:59.325306Z", "start_time": "2024-04-18T08:25:59.202454Z" } }, "outputs": [], "source": [ "import numba" ] }, { "cell_type": "code", "execution_count": 39, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:00.185673Z", "start_time": "2024-04-18T08:26:00.177083Z" } }, "outputs": [], "source": [ "pairwise_numba = numba.jit(pairwise_python)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Tradiční kontrola. Po prvním spuštění navíc Numba funkci poprvé zkompiluje." ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:01.849016Z", "start_time": "2024-04-18T08:26:01.381482Z" }, "collapsed": false }, "outputs": [], "source": [ "assert np.allclose(pairwise_numpy(X), pairwise_numba(X), rtol=1e-10, atol=1e-15)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Jaký čas od takto \"jednoduché\" optimalizace můžeme očekávat?" ] }, { "cell_type": "code", "execution_count": 41, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:05.311692Z", "start_time": "2024-04-18T08:26:03.651744Z" }, "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2.48 ms ± 65 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "timings = %timeit -o pairwise_numba(X)\n", "pairwise_times['numba'] = timings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vidíme, že zrychlení je výborné - jsme na úrovni zatím nejlepšího výsledku! A navíc že jsme toho dosáhli jediným řádkem (kromě importů)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ještě můžeme zkusit výledek vylepšit pomocí paralelizace, `nopython` a / nebo `fastmath` režimu. Pro `nopython` musíme vytvořit výsledný numpy objekt vně kompilované funkce. Paralelizace docílíme pomocí `parallel=True` a [`numba.prange`](http://numba.pydata.org/numba-doc/latest/user/parallel.html?highlight=prange). Všimněte si použití `@jit` jako dekorátoru." ] }, { "cell_type": "code", "execution_count": 42, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:07.690637Z", "start_time": "2024-04-18T08:26:07.686997Z" } }, "outputs": [], "source": [ "@numba.jit(nopython=True, parallel=True, fastmath=True)\n", "def _pairwise_nopython(X: np.ndarray, D: np.ndarray) -> np.ndarray:\n", " M = X.shape[0]\n", " N = X.shape[1]\n", " for i in numba.prange(M):\n", " for j in numba.prange(M):\n", " d = 0.0\n", " for k in range(N):\n", " tmp = X[i, k] - X[j, k]\n", " d += tmp * tmp\n", " D[i, j] = np.sqrt(d)\n", " return D\n", "\n", "\n", "def pairwise_numba_fast_parallel(X: np.ndarray) -> np.ndarray:\n", " D = np.empty((X.shape[0], X.shape[0]), dtype = float)\n", " _pairwise_nopython(X, D)\n", " return D" ] }, { "cell_type": "code", "execution_count": 43, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:08.828091Z", "start_time": "2024-04-18T08:26:08.498511Z" } }, "outputs": [], "source": [ "assert np.allclose(pairwise_numpy(X), pairwise_numba_fast_parallel(X), rtol=1e-10, atol=1e-15)" ] }, { "cell_type": "code", "execution_count": 44, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:13.446459Z", "start_time": "2024-04-18T08:26:08.927949Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "711 μs ± 33.1 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n" ] } ], "source": [ "timings = %timeit -o pairwise_numba_fast_parallel(X)\n", "pairwise_times['numba_fast_parallel'] = timings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Pojdme počítat na GPU i CPU: JAX\n", "\n", "_JAX: High-Performance Array Computing_ poskytuje plnou paletu nastrojů pro výpočty jak na CPU, tak na GPU. \n", "\n", "Mezi jeho hlavní výhody patří:\n", "\n", "* NumPy-like API - JAX podporuje většinu NumPy operací.\n", "* JIT kompilace - JAX umožňuje, podobně jako `numba`, kompilaci funkcí pomocí `jax.jit`.\n", "* Běží všude - stejný kód může běžet na CPU, GPU i TPU.\n", "* Automatická diferenciace - JAX umožňuje výpočet gradientů a Hessiánů.\n", "\n", " " ] }, { "cell_type": "code", "execution_count": 45, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:14.973233Z", "start_time": "2024-04-18T08:26:14.970674Z" } }, "outputs": [], "source": [ "# Odkomentujte pro instalaci balíku JAX\n", "# !pip install jax[cpu] jaxlib" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "JAX NumPy API je téměř identické s NumPy API. Jediný rozdíl je, že JAX funkce vrací vlastní datovy typ, které reprezentují pole na určitém zařízení (CPU, GPU, TPU).).\n", "Je proto vyhodné importovat JAX jako `import jax.numpy as jnp` společně s NumPy jako `import numpy as np`." ] }, { "cell_type": "code", "execution_count": 47, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:16.447427Z", "start_time": "2024-04-18T08:26:15.990599Z" } }, "outputs": [], "source": [ "import jax.numpy as jnp\n", "import jax" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "V případě, že bychom měli dostupné GPU (AMD/NVIDIA), vyděli bychom jej v nasledujícím seznamu. " ] }, { "cell_type": "code", "execution_count": 48, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:18.876047Z", "start_time": "2024-04-18T08:26:18.859776Z" } }, "outputs": [ { "data": { "text/plain": [ "[CpuDevice(id=0)]" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "jax.devices()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pro zpřístupnění vypočtů na GPU stačí nahradit `np` za `jnp` a JAX se postará o zbytek." ] }, { "cell_type": "code", "execution_count": 49, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:21.002794Z", "start_time": "2024-04-18T08:26:20.999454Z" } }, "outputs": [], "source": [ "def pairwise_jax(X):\n", " return jnp.sqrt(jnp.power(X[:, jnp.newaxis, :] - X, 2).sum(-1))\n", " #return jnp.sqrt((jnp.power(X[:, jnp.newaxis, :] - X, 2)).sum(-1))\n" ] }, { "cell_type": "code", "execution_count": 50, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:22.355890Z", "start_time": "2024-04-18T08:26:22.156958Z" } }, "outputs": [ { "ename": "AssertionError", "evalue": "", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[50], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m np\u001b[38;5;241m.\u001b[39mallclose(pairwise_numpy(X), pairwise_jax(X), rtol\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1e-10\u001b[39m, atol\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1e-15\u001b[39m)\n", "\u001b[0;31mAssertionError\u001b[0m: " ] } ], "source": [ "assert np.allclose(pairwise_numpy(X), pairwise_jax(X), rtol=1e-10, atol=1e-15)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ouha, co je špatně? Jelikož GPU pracuje s nižší přesností, JAX pracuje v zakladním režimu `float32` namísto `float64`." ] }, { "cell_type": "code", "execution_count": 51, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:34.792567Z", "start_time": "2024-04-18T08:26:34.768471Z" } }, "outputs": [ { "data": { "text/plain": [ "dtype('float32')" ] }, "execution_count": 51, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pairwise_jax(X).dtype" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pro vypočty s 64-bitovou přesností je potřeba povolit `jax_enable_x64`." ] }, { "cell_type": "code", "execution_count": 52, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:36.962400Z", "start_time": "2024-04-18T08:26:36.959692Z" } }, "outputs": [], "source": [ "from jax import config\n", "config.update(\"jax_enable_x64\", True)" ] }, { "cell_type": "code", "execution_count": 53, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:40.204841Z", "start_time": "2024-04-18T08:26:40.108146Z" } }, "outputs": [ { "data": { "text/plain": [ "dtype('float64')" ] }, "execution_count": 53, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pairwise_jax(X).dtype" ] }, { "cell_type": "code", "execution_count": 54, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:41.465030Z", "start_time": "2024-04-18T08:26:41.383496Z" } }, "outputs": [], "source": [ "assert np.allclose(pairwise_numpy(X), pairwise_jax(X), rtol=1e-10, atol=1e-15)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Proj JIT kompilaci stačí přidat dekorátor `jax.jit`. \n", "\n", "Nicméně pozor: \n", "* První volání funkce s JIT kompilací může byt pomalejší, než když bychom funkci volali bez kompilace.\n", "* JIT vyžaduje znát typy a tvar (shape) vstupních parametrů v době kompilace." ] }, { "cell_type": "code", "execution_count": 55, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:43.629525Z", "start_time": "2024-04-18T08:26:43.626418Z" } }, "outputs": [], "source": [ "@jax.jit\n", "def pairwise_jax_jit(X):\n", " res = (jnp.sqrt((jnp.power(X[:, jnp.newaxis, :] - X, 2)))).sum(-1)\n", " return res\n" ] }, { "cell_type": "code", "execution_count": 56, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:44.614037Z", "start_time": "2024-04-18T08:26:44.611318Z" } }, "outputs": [], "source": [ "X_jnp = np.asarray(X, dtype=jnp.float64)" ] }, { "cell_type": "code", "execution_count": 57, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:47.551431Z", "start_time": "2024-04-18T08:26:45.087129Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "10.7 ms ± 17 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" ] } ], "source": [ "timings = %timeit -o pairwise_jax(X_jnp)\n", "pairwise_times['jax'] = timings" ] }, { "cell_type": "code", "execution_count": 59, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:55.285580Z", "start_time": "2024-04-18T08:26:50.089028Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "663 μs ± 9.51 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n" ] } ], "source": [ "timings = %timeit -o pairwise_jax_jit(X_jnp)\n", "pairwise_times['jax_jit'] = timings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Srovnání výsledků\n", "\n", "Výsledky můžeme porovnat pomocí grafu." ] }, { "cell_type": "code", "execution_count": 60, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:26:59.171339Z", "start_time": "2024-04-18T08:26:59.167016Z" } }, "outputs": [ { "data": { "text/plain": [ "{'plain_python': ,\n", " 'numpy': ,\n", " 'cython0': ,\n", " 'cython1': ,\n", " 'cython2': ,\n", " 'fortran': ,\n", " 'numba': ,\n", " 'numba_fast_parallel': ,\n", " 'jax': ,\n", " 'jax_jit': }" ] }, "execution_count": 60, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pairwise_times" ] }, { "cell_type": "code", "execution_count": 61, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:27:01.532941Z", "start_time": "2024-04-18T08:27:01.208395Z" }, "collapsed": false }, "outputs": [ { "data": { "image/png": "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gJOee+45+f333+WKK66Q3NxcOf300+XHH3+UxMREmT59uvTt2zfsJsIBBCEAQMxMo//yyy8lNTXVbEANVAVBCADgvMhXWVxcXNhNgWOoEQIAOOvJJ5+UDh06SN26dc2ht6dOnRp2s+AQps8DAJw0atQoycnJkaFDh0rXrl3NuYULF8oNN9xgVpoeM2ZM2E2EAxgaAwA4qVmzZjJx4kS55JJLypx//vnnTTgqKCgIrW1wB0NjAAAn/fbbb2YxxfI6depkZpMBVUEQAgA4qV+/fvLYY49VOD9lyhS57LLLQmkT3EONEADAGcOHDy+5rTPEtDD67bffluOOO86c033HtD6of//+IbYSLqFGCADgjKysrCo9T0OS7kUG7AlBCAAAeIsaIQAA4C1qhAAATvr111/l4Ycflvnz50teXp4UFxeXeZwd6FEVBCEAgJOuvPJKUyh9wQUXSJcuXdheA38INUIAACclJSXJnDlz5IQTTgi7KXAYNUIAACe1atVKGjRoEHYz4DiCEADASePHj5cRI0bIunXrwm4KHEaNEADASbq9hhZMt23bVurXry+1a9cu8/iPP/4YWtvgDoIQAMBJutnq999/L/fee680b96cYmn8IRRLAwCcpL1ACxculIyMjLCbAodRIwQAcFL79u3ll19+CbsZcBxBCADgpHHjxsmNN94o//73v2Xz5s1SWFhY5gCqgqExAICT4uP/71q+fG2Qfq3puaKiopBaBpdQLA0AcJJurQH8WfQIAQAAb9EjBABw0oIFC3b7+Mknn1xjbYG76BECADhdI1Ra6XohaoRQFcwaAwA46aeffipz5OXlydy5cyUzM9PsSg9UBT1CAICY8v7778vw4cMlNzc37KbAAfQIAQBiim638dVXX4XdDDiCYmkAgJM+++yzMvd1gOOHH34wCy127NgxtHbBLQyNAQCcLZbW4ujyX2PHHXecTJs2zWzBAewJQQgA4KR169ZVCEbNmjWTunXrhtYmuIcgBABw1rx588yhM8aKi4vLPKa9QsCeUCMEAHDSnXfeKWPGjJHOnTvLgQceWGHPMaAq6BECADhJw8/9998v/fr1C7spcBjT5wEATtq1a5ccf/zxYTcDjiMIAQCcdNVVV8mMGTPCbgYcR40QAMBJv/76q0yZMkXeffddOeqoo6R27dplHs/JyQmtbXAHNUIAACdlZWVFfUwLp997770abQ/cRBACAADeokYIAAB4iyAEAAC8RRACAADeIggBAABvEYQAAIC3CEIAAMBbBCEAAOAtghAAABBf/X+zPvr+YOBEOgAAAABJRU5ErkJggg==", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig, ax = plt.subplots()\n", "values = np.array([t.average for t in pairwise_times.values()])\n", "x = range(len(pairwise_times))\n", "ax.bar(x, values)\n", "ax.set_xticks(x)\n", "ax.set_xticklabels(tuple(pairwise_times.keys()), rotation='vertical')\n", "ax.set_ylabel('time [ms]')\n", "ax.set_yscale('log')" ] }, { "cell_type": "code", "execution_count": 62, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:31:41.312579Z", "start_time": "2024-04-18T08:31:41.309966Z" } }, "outputs": [], "source": [ "### Důkladnější srovnaní:\n", "pairwise_functions = {\n", " 'plain_python': pairwise_python,\n", " 'numpy': pairwise_numpy,\n", " 'cython0': cyfuncs.pairwise0,\n", " 'cython1': cyfuncs.pairwise1,\n", " 'cython2': pairwise_cython,\n", " 'fortran': pairwise_fort,\n", " 'numba': pairwise_numba,\n", " 'numba_fast_parallel': pairwise_numba_fast_parallel,\n", " 'jax': pairwise_jax,\n", " 'jax_jit': pairwise_jax_jit\n", "}" ] }, { "cell_type": "code", "execution_count": 63, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:43:49.848782Z", "start_time": "2024-04-18T08:41:23.293147Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "plain_python\n", "10\n", "111 μs ± 544 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "31\n", "1.1 ms ± 45.6 μs per loop (mean ± std. dev. of 2 runs, 1,000 loops each)\n", "100\n", "11.4 ms ± 415 μs per loop (mean ± std. dev. of 2 runs, 100 loops each)\n", "316\n", "109 ms ± 658 μs per loop (mean ± std. dev. of 2 runs, 10 loops each)\n", "1000\n", "1.1 s ± 4.75 ms per loop (mean ± std. dev. of 2 runs, 1 loop each)\n", "3162\n", "11.2 s ± 51.6 ms per loop (mean ± std. dev. of 2 runs, 1 loop each)\n", "numpy\n", "10\n", "3.96 μs ± 0.608 ns per loop (mean ± std. dev. of 2 runs, 100,000 loops each)\n", "31\n", "19 μs ± 527 ns per loop (mean ± std. dev. of 2 runs, 100,000 loops each)\n", "100\n", "208 μs ± 10.8 μs per loop (mean ± std. dev. of 2 runs, 1,000 loops each)\n", "316\n", "1.85 ms ± 1.49 μs per loop (mean ± std. dev. of 2 runs, 1,000 loops each)\n", "1000\n", "20.5 ms ± 382 μs per loop (mean ± std. dev. of 2 runs, 10 loops each)\n", "3162\n", "199 ms ± 3.42 ms per loop (mean ± std. dev. of 2 runs, 1 loop each)\n", "cython0\n", "10\n", "106 μs ± 828 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "31\n", "1.01 ms ± 13 μs per loop (mean ± std. dev. of 2 runs, 1,000 loops each)\n", "100\n", "10.3 ms ± 5.73 μs per loop (mean ± std. dev. of 2 runs, 100 loops each)\n", "316\n", "104 ms ± 71.3 μs per loop (mean ± std. dev. of 2 runs, 10 loops each)\n", "1000\n", "1.05 s ± 6.76 ms per loop (mean ± std. dev. of 2 runs, 1 loop each)\n", "3162\n", "10.6 s ± 110 ms per loop (mean ± std. dev. of 2 runs, 1 loop each)\n", "cython1\n", "10\n", "6.62 μs ± 4.32 ns per loop (mean ± std. dev. of 2 runs, 100,000 loops each)\n", "31\n", "60.4 μs ± 9.07 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "100\n", "623 μs ± 1.36 μs per loop (mean ± std. dev. of 2 runs, 1,000 loops each)\n", "316\n", "6.65 ms ± 48.7 μs per loop (mean ± std. dev. of 2 runs, 100 loops each)\n", "1000\n", "81.7 ms ± 2.61 ms per loop (mean ± std. dev. of 2 runs, 10 loops each)\n", "3162\n", "787 ms ± 20.3 ms per loop (mean ± std. dev. of 2 runs, 1 loop each)\n", "cython2\n", "10\n", "982 ns ± 4.6 ns per loop (mean ± std. dev. of 2 runs, 1,000,000 loops each)\n", "31\n", "2.56 μs ± 1.39 ns per loop (mean ± std. dev. of 2 runs, 100,000 loops each)\n", "100\n", "17.1 μs ± 1.1 ns per loop (mean ± std. dev. of 2 runs, 100,000 loops each)\n", "316\n", "160 μs ± 93.1 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "1000\n", "1.95 ms ± 1.53 μs per loop (mean ± std. dev. of 2 runs, 1,000 loops each)\n", "3162\n", "20.3 ms ± 193 μs per loop (mean ± std. dev. of 2 runs, 10 loops each)\n", "fortran\n", "10\n", "501 ns ± 0.712 ns per loop (mean ± std. dev. of 2 runs, 1,000,000 loops each)\n", "31\n", "2.29 μs ± 2.48 ns per loop (mean ± std. dev. of 2 runs, 100,000 loops each)\n", "100\n", "20.4 μs ± 48.4 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "316\n", "201 μs ± 680 ns per loop (mean ± std. dev. of 2 runs, 1,000 loops each)\n", "1000\n", "2.32 ms ± 25.3 μs per loop (mean ± std. dev. of 2 runs, 100 loops each)\n", "3162\n", "22.8 ms ± 251 μs per loop (mean ± std. dev. of 2 runs, 10 loops each)\n", "numba\n", "10\n", "604 ns ± 1.75 ns per loop (mean ± std. dev. of 2 runs, 1,000,000 loops each)\n", "31\n", "2.37 μs ± 3.2 ns per loop (mean ± std. dev. of 2 runs, 100,000 loops each)\n", "100\n", "20 μs ± 125 ns per loop (mean ± std. dev. of 2 runs, 100,000 loops each)\n", "316\n", "192 μs ± 1.5 μs per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "1000\n", "2.39 ms ± 34.1 μs per loop (mean ± std. dev. of 2 runs, 100 loops each)\n", "3162\n", "24 ms ± 27.8 μs per loop (mean ± std. dev. of 2 runs, 10 loops each)\n", "numba_fast_parallel\n", "10\n", "95.4 μs ± 126 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "31\n", "96 μs ± 293 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "100\n", "99.9 μs ± 19.5 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "316\n", "140 μs ± 5.56 μs per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "1000\n", "655 μs ± 762 ns per loop (mean ± std. dev. of 2 runs, 1,000 loops each)\n", "3162\n", "5.7 ms ± 6.79 μs per loop (mean ± std. dev. of 2 runs, 100 loops each)\n", "jax\n", "10\n", "43.3 μs ± 4.18 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "31\n", "73.4 μs ± 1.52 μs per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "100\n", "161 μs ± 557 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "316\n", "1.41 ms ± 2.31 μs per loop (mean ± std. dev. of 2 runs, 1,000 loops each)\n", "1000\n", "10.6 ms ± 1.3 μs per loop (mean ± std. dev. of 2 runs, 100 loops each)\n", "3162\n", "128 ms ± 2.93 ms per loop (mean ± std. dev. of 2 runs, 10 loops each)\n", "jax_jit\n", "10\n", "6.17 μs ± 12.4 ns per loop (mean ± std. dev. of 2 runs, 100,000 loops each)\n", "31\n", "17.4 μs ± 7.84 ns per loop (mean ± std. dev. of 2 runs, 100,000 loops each)\n", "100\n", "20.7 μs ± 19.4 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "316\n", "99.5 μs ± 206 ns per loop (mean ± std. dev. of 2 runs, 10,000 loops each)\n", "1000\n", "649 μs ± 82.6 ns per loop (mean ± std. dev. of 2 runs, 1,000 loops each)\n", "3162\n", "6.1 ms ± 11.6 μs per loop (mean ± std. dev. of 2 runs, 100 loops each)\n" ] } ], "source": [ "\n", "Ms = np.logspace(1, 3.5, 6).astype(int)\n", "N = 3\n", "\n", "paiwise_times_all = {}\n", "for name, func in pairwise_functions.items():\n", " print(name)\n", " timings = []\n", " for M in Ms:\n", " X = np.random.random((M, N))\n", " print(M)\n", " # t = %timeit -o func(X)\n", " if \"jax\" in name:\n", " X_jnp = np.asarray(X, dtype=jnp.float64)\n", " # Přeskočíme první měření, protože probíha kompilace\n", " func(X_jnp)\n", " t = %timeit -o -r 2 func(X_jnp)\n", " else:\n", " t = %timeit -o -r 2 func(X)\n", "\n", " timings.append(t.average)\n", "\n", " paiwise_times_all[name] = timings\n" ] }, { "cell_type": "code", "execution_count": 64, "metadata": { "ExecuteTime": { "end_time": "2024-04-18T08:48:52.464845Z", "start_time": "2024-04-18T08:48:52.062914Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 64, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig, ax = plt.subplots()\n", "\n", "for name, times in paiwise_times_all.items():\n", " ax.plot(Ms, times, label=name)\n", " ax.set_xscale('log')\n", " ax.set_yscale('log')\n", "\n", "ax.set_xlabel('M')\n", "ax.set_ylabel('time [s]')\n", "\n", "ax.legend()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Další možnosti\n", "\n", "* Vestavěný modul [`ctypes`](http://docs.python.org/2/library/ctypes.html) dovoluje volat funkce z externích dynamických knihoven. Pro použití s NumPy viz [Cookbook](http://wiki.scipy.org/Cookbook/Ctypes).\n", "* Alternativou k ctypes je [cffi](https://pypi.org/project/cffi/).\n", "* [CuPy](https://cupy.chainer.org/) využívá GPU.\n", "* [numexpr](https://github.com/pydata/numexpr) dokáže kompilovat Numpy výrazy.\n", "* [Theano](http://www.deeplearning.net/software/theano/index.html) se zaměřuje na strojové učení, také optimalizuje vektorové (maticové) operace, dovoluje je spouštět na CPU.\n", "* [Nuitka](http://nuitka.net/) kompiluje Python, ale na rozdíl od Numba nespecializuje funkce na základě typů.\n", "* [SWIG](http://www.swig.org/) jde použít pro propojení s mnoha jazyky.'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Cvičení\n", "\n", "Obdobný postup aplikujte na výpočet kumulativního součtu, který je definovaný jako\n", "\n", "$\\displaystyle\n", "S_j = \\sum\\limits_{i = 1}^j x_i $\n", "\n", "Výsledky a časování porovnejte s `numpy.cumsum`.\n", "\n", "*Nápověda: Ve vaší funkci vytvořte nejprve výsledné numpy pole pomocí `numpy.empty_like`.*" ] } ], "metadata": { "anaconda-cloud": {}, "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.12.5" } }, "nbformat": 4, "nbformat_minor": 2 }