Как нагрузить процессор python
Всегда знал, что одно из достоинств Python — возможность переписать самые тормозные куски кода на Си и увеличить быстродействие программы до недостижимых интерпретируемым языкам высот. Но сам ни разу не пробовал, т.к. считал что это слишком сложно. После прочтения этой статьи больше так не считаю.
Программисты знакомые с ctypes врядли найдут тут что-то интересное, новичков же прошу под кат.
Ctypes — механизм Python для импорта функций из внешних библиотек.
%timeit — magic-функция оболочки IPython, измеряющая время выполнения выражения на Python
Ctypes — это прекрасно! Давайте начнем с небольшого банального примера: суммирование чисел в определенном диапазоне.
Вот реализация этой функции на Python
Отлично! Но что если мы попробуем суммировать действительно большой диапазон чисел, например от 0 до 10**8 (т.е. 100,000,000)
Уже не так весело. Попробуем кое-что другое:
Что из этого получится?
Вот это да… Так еще хуже… В этот раз даже не буду пробовать 10**9.
Так как же нам ускорить выполнение? Только не предлагайте математические оптимизации… мы же в новом мире компьютеров! (в оригинале: don't suggest math tricks… this is the the new world of computing!)
Да, я знаю, что сложность алгоритма — постоянная величина и не зависит о величины аргумента, n*(n+1)/2. Но статья посвящена не этому.
Как насчет ctypes?
Сохраним с именем sumrange.c и скомпилируем (не будем использовать оптимизации для чистоты эксперимента):
$ gcc -shared -Wl,-install_name,sumrange.so -o sumrange.so -fPIC sumrange.c
Импортируем в Python то что получилось:
И Оскар получает…
Итоговая сводка:
10**2 | 10**8 | 10**9 | 10**10 | |
---|---|---|---|---|
Чистый Python, способ №1 | 1.53 мкс | 9.77 с | 97.8 с | - |
Чистый Python, способ №2 | 10.5 мкс | 18.5 с | - | - |
ctypes | 1.28 мкс | 381 мс | 3.79 с | 37.8 с |
Адский прирост производительности!
HPC benchmarks for Python
This is a suite of benchmarks to test the sequential CPU and GPU performance of various computational backends with Python frontends.
Specifically, we want to test which high-performance backend is best for geophysical (finite-difference based) simulations.
Contents
The scientific Python ecosystem is thriving, but high-performance computing in Python isn't really a thing yet. We try to change this with our pure Python ocean simulator Veros, but which backend should we use for computations?
Tremendous amounts of time and resources go into the development of Python frontends to high-performance backends, but those are usually tailored towards deep learning. We wanted to see whether we can profit from those advances, by (ab-)using these libraries for geophysical modelling.
Why do the benchmarks look so weird?
These are more or less verbatim copies from Veros (i.e., actual parts of a physical model). Most earth system and climate model components are based on finite-difference schemes to compute derivatives. This can be represented in vectorized form by index shifts of arrays (such as 0.5 * (arr[1:] + arr[:-1]) , the first-order derivative of arr at every point). The most common index range is [2:-2] , which represents the full domain (the two outermost grid cells are overlap / "ghost cells" that allow us to shift the array across the boundary).
Now, maths is difficult, and numerics are weird. When many different physical quantities (defined on different grids) interact, things get messy very fast.
Why only test sequential CPU performance?
- I was curious to see how good the compilers are without being able to fall back to thread parallelism.
- In many physical models, it is pretty straightforward to parallelize the model "by hand" via MPI. Therefore, we are not really dependent on good parallel performance out of the box.
Which backends are currently supported?
(not every backend is available for every benchmark)
What is included in the measurements?
Pure time spent number crunching. Preparing the inputs, copying stuff from and to GPU, compilation time, time it takes to check results etc. are excluded. This is based on the assumption that these things are only done a few times per simulation (i.e., that their cost is amortized during long-running simulations).
How does this compare to a low-level implementation?
As a rule of thumb (from our experience with Veros), the performance of a Fortran implementation is very close to that of the Numba backend, or ~3 times faster than NumPy.
If you prefer to install things by hand, just have a look at the environment files to see what you need. You don't need to install all backends; if a module is unavailable, it is skipped automatically.
Your entrypoint is the script run.py :
Benchmarks are run for all combinations of the chosen sizes ( -s ) and backends ( -b ), in random order.
Some backends refuse to be confined to a single thread, so I recommend you wrap your benchmarks in taskset to set processor affinity to a single core (only works on Linux):
Some backends use all available GPUs by default, some don't. If you have multiple GPUs, you can set the one to be used through CUDA_VISIBLE_DEVICES , so keep things fair.
Some backends are greedy with allocating memory. On GPU, you can only run one backend at a time (add NumPy for reference):
Equation of state
Turbulent kinetic energy
Lessons I learned by assembling these benchmarks: (your mileage may vary)
- The performance of JAX is very competitive, both on GPU and CPU. It is consistently among the top implementations on both platforms.
- Pytorch performs very well on GPU for large problems (slightly better than JAX), but its CPU performance is not great for tasks with many slicing operations.
- Numba is a great choice on CPU if you don't mind writing explicit for loops (which can be more readable than a vectorized implementation), being slightly faster than JAX with little effort.
- JAX performance on GPU seems to be quite hardware dependent. JAX performancs significantly better (relatively speaking) on a Tesla P100 than a Tesla K80.
- If you have embarrasingly parallel workloads, speedups of > 1000x are easy to achieve on high-end GPUs.
- TPUs are catching up to GPUs. We can now get similar performance to a high-end GPU on these workloads.
- Tensorflow is not great for applications like ours, since it lacks tools to apply partial updates to tensors (such as tensor[2:-2] = 0. ).
- If you use Tensorflow on CPU, make sure to use XLA ( experimental_compile ) for tremendous speedups.
- CuPy is nice! Often you don't need to change anything in your NumPy code to have it run on GPU (with decent, but not outstanding performance).
- Reaching Fortran performance on CPU for non-trivial tasks is hard :)
Community contributions are encouraged! Whether you want to donate another benchmark, share your experience, optimize an implementation, or suggest another backend - feel free to ask or open a PR.
Adding a new backend
Adding a new backend is easy!
Let's assume that you want to add support for a library called speedygonzales . All you need to do is this:
Implement a benchmark to use your library, e.g. benchmarks/equation_of_state/eos_speedygonzales.py .
Register the benchmark in the respective __init__.py file ( benchmarks/equation_of_state/__init__.py ), by adding "speedygonzales" to its __implementations__ tuple.
Register the backend, by adding its setup function to the __backends__ dict in backends.py .
A setup function is what is called before every call to your benchmark, and can be used for custom setup and teardown. In the simplest case, it is just
Then, you can run the benchmark with your new backend:
Чего ожидает программа?
В те моменты, когда программа не занята интенсивными вычислениями с использованием процессора, она, по всей видимости, чего-то ждёт. Вот чем может быть вызвано бездействие программы:
- Сетевые ресурсы. Сюда может входить ожидание завершения операций DNS-поиска, ожидание ответа некоего сетевого ресурса, ожидание завершения загрузки неких данных и так далее.
- Жёсткий диск. Чтение данных с жёсткого диска может занимать некоторое время. То же самое можно сказать и о записи на диск. Иногда операции чтения или записи выполняются лишь с использованием кэша, расположенного в оперативной памяти. При таком подходе всё происходит довольно-таки быстро. Но иногда, когда программа напрямую взаимодействует с диском, подобные операции оказываются достаточно медленными.
- Блокировки. Программа может ожидать снятия блокировки потока или процесса.
- Приостановка работы. Иногда программа может преднамеренно приостановить работу, например, делая паузу между попытками выполнить некое действие.
Способ №1: анализ времени, в течение которого программа не пользуется процессором
Встроенный профилировщик Python, cProfile , умеет собирать данные по множеству различных показателей, имеющих отношение к работе программ. Благодаря этому его можно использовать для создания инструмента, с помощью которого можно проанализировать время, в течение которого программа не пользуется ресурсами процессора.
Операционная система может сообщить нам о том, сколько именно процессорного времени использовала программа.
Представим, что мы профилируем однопоточную программу. Многопоточные программы сложнее профилировать, да и описывать этот процесс тоже непросто. Если программа выполнялась 9 секунд и при этом пользовалась процессором 7.5 секунд — это означает, что она потратила 1.5 секунды на ожидание.
Для начала создадим таймер, который будет измерять время ожидания:
Затем создадим профилировщик, который выполняет анализ этого времени:
После этого можно профилировать различные функции:
Полученные результаты позволяют сделать вывод о том, что большая часть времени потрачена на чтение данных из сокета, но некоторое время ушло на выполнение DNS-поиска ( getaddrinfo ), а также на выполнение TCP-рукопожатия ( connect ) и TLS/SSL-рукопожатия.
Так как мы позаботились о том, чтобы исследовать те периоды работы программы, в которые она не пользуется ресурсами процессора, мы знаем, что всё это — чистое время ожидания, то есть — время, когда программа не занята какими-либо вычислениями.
Почему тут присутствует время, записанное для str.find и list.append ? При выполнении подобных операций программе нечего ждать, поэтому правдоподобно выглядит объяснение, в соответствии с которым мы имеем дело с ситуацией, когда не выполнялся весь процесс. Возможно — ожидая окончания выполнения какого-то другого процесса, или ожидая завершения загрузки данных в память из файла подкачки. Это указывает на то, что на выполнение данных операций было затрачено некоторое время, которое не входит в состав процессорного времени.
Кроме того, хочу отметить, что мне доводилось видеть отчёты, в которых имеются небольшие отрицательные фрагменты времени. Это предполагает наличие определённого несоответствия между прошедшим временем и процессорным временем, но я не жду, что это окажет значительное воздействие на анализ более сложных программ.
Python + NumPy + SciPy
Проблему медленности Python для численных задач осознали очень давно. И ответом на эту проблему была библиотека NumPy. Идеология NumPy во многом близка MatLab, который является общепризнанным инструментом научных расчетов.
Мы прекращаем мыслить итерационно, мы начинаем мыслить матрицами и векторами как атомарными объектами для вычисления. А все операции с матрицам и векторами на нижнем уровне уже выполняются высокопроизводительными библиотеками линейной алгебры Intel MKL или ATLAS.
Реализация на NumPy выглядит так:
В этой реализации вообще нет ни одного цикла!
Время выполнения: 839 мс, что медленнее базовой реализации где-то в 19 раз.
Более того, в NumPy и SciPy есть огромное количество встроенных функций. Реализация данной задачи на SciPy выглядит так:
Время выполнения: 353 мс, что медленнее базовой реализации в 8 раз.
Для большинства задач это уже вполне приемлемое время работы. Ценой этого является переключение способа мышления, теперь необходимо собирать код из базовых операций линейной алгебры. Иногда это выглядит очень красиво, но порой приходится придумывать разные трюки.
Но а как быть с параллельностью? Здесь она неявная. Мы надеемся, что на низком уровне все операции с матрицами и векторами реализованы эффективно и параллельно.
А что делать, если наш код не вписывается в линейную алгебру, или мы хотим явную параллелизацию?
Введение
В настоящее время Python очень активно используется в научных вычислениях, а в области Machine Learning вообще является практически одним из стандартов. Но если посмотреть чуть глубже, то почти везде Python используется как обертка над библиотеками более низкого уровня, написанных большей частью на C/C++. А можно ли на чистом Python писать на самом деле быстрый и параллельный код?
Рассмотрим совсем простую задачу. Пусть нам даны два набора N точек в трехмерном пространстве: p и q. Необходимо вычислить специальную матрицу на основе попарных расстояний между всеми точками:
Для всех тестов возьмем N = 5000. Время вычисления усредняется для 10 запусков.
Реализация на C++
Как точку отсчета возьмем следующую реализацию на C++:
Внешний цикл по точкам p выполняется параллельно с использованием технологии OpenMP.
Время выполнения: 44 мс.
About
A suite of benchmarks for CPU and GPU performance of the most popular high-performance libraries for Python 🚀
Py-cpuinfo gets CPU info with pure Python. Py-cpuinfo should work without any extra programs or libraries, beyond what your OS provides. It does not require any compilation(C/C++, assembly, et cetera) to use. It works with Python 2 and 3.
key | Example value | Return Format |
---|---|---|
"python_version" | "2.7.12.final.0 (64 bit)" | string |
"cpuinfo_version" | (4, 0, 0) | (int, int, int) |
"cpuinfo_version_string" | "4.0.0" | string |
"hz_advertised_friendly" | "2.9300 GHz" | string |
"hz_actual_friendly" | "1.7330 GHz" | string |
"hz_advertised" | (2930000000, 0) | (int, int) |
"hz_actual" | (1733000000, 0) | (int, int) |
"arch" | "X86_64" | "X86_32", "X86_64", "ARM_8", "ARM_7", "PPC_32", "PPC_64", "SPARC_32", "SPARC_64", "S390X", "MIPS_32", "MIPS_64", "RISCV_32", "RISCV_64" |
"bits" | 64 | int |
"count" | 4 | int |
"l1_data_cache_size" | 32768 | int |
"l1_instruction_cache_size" | 32768 | int |
"l2_cache_size" | 262144 | int |
"l2_cache_line_size" | 256 | int |
"l2_cache_associativity" | 6 | int |
"l3_cache_size" | 3145728 | int |
"stepping" | 5 | int |
"model" | 30 | int |
"family" | 6 | int |
"processor_type" | 0 | int |
"flags" | ['acpi', 'aperfmperf', 'apic', 'arch_perfmon', 'bts', 'clflush', 'cmov', 'constant_tsc', 'cx16', 'cx8', 'de', 'ds_cpl', 'dtes64', 'dtherm', 'dts', 'ept', 'est', 'flexpriority', 'fpu', 'fxsr', 'ht', 'ida', 'lahf_lm', 'lm', 'mca', 'mce', 'mmx', 'monitor', 'msr', 'mtrr', 'nonstop_tsc', 'nopl', 'nx', 'pae', 'pat', 'pbe', 'pdcm', 'pebs', 'pge', 'pni', 'popcnt', 'pse', 'pse36', 'rdtscp', 'rep_good', 'sep', 'smx', 'ss', 'sse', 'sse2', 'sse4_1', 'sse4_2', 'ssse3', 'syscall', 'tm', 'tm2', 'tpr_shadow', 'tsc', 'vme', 'vmx', 'vnmi', 'vpid', 'xtopology', 'xtpr'] | [string] |
These fields are pulled directly from the CPU and are unverified. They may contain expected results. Other times they may contain wildly unexpected results or garbage. So it would be a bad idea to rely on them.
Совсем недавно вышла новая версия 0.34 библиотеки оптимизирующего JIT компилятора Numba для Python. И там ура! появилась долгожданная семантика аннотаций и набор методов для организации параллельных вычислений. За основу была взята технология Intel Parallel Accelerator.
В данной статье я хочу поделиться результатами первого тестирования скорости вычислений на основе этой библиотеки для некоторой современной машины с четырехядерным процессором.
Python + Numba
Мы проделали длинный путь. Мы начали с чистого Python, потом пошли по пути магии матричных вычислений, потом погрузились в специальных язык расширений. Пора вернуться обратно к тому, с чего мы начали. Итак, реализация на Python + Numba:
Время выполнения: 46 мс, что практически совпадает с базовой реализацией!
И все, что нужно было сделать для этого с исходным медленным Python кодом:
Предположим, ваша Python-программа оказалась медленной, и вы выяснили, что это лишь отчасти обусловлено нехваткой процессорных ресурсов. Как выяснить то, какие части кода вынуждены ожидать чего-то такого, что не относится к CPU?
Прочтя материал, перевод которого мы публикуем сегодня, вы узнаете о том, как писать собственные профилировщики для Python-кода. Речь идёт об инструментах, которые позволят обнаружить места в коде, которые бездействуют в ожидании освобождения неких ресурсов. В частности, мы обсудим здесь следующее:
- Чего может ожидать программа?
- Профилировка использования ресурсов, не являющихся ресурсами CPU.
- Профилировка непреднамеренных переключений контекста.
Итоги
Использование описанной здесь методики профилирования кода создаёт определённую дополнительную нагрузку на систему, что сильно замедляет программу. В большинстве случаев, однако, это не должно привести к значительному искажению результатов из-за того, что мы не выполняем анализ использования ресурсов процессора.
В целом же можно отметить, что профилированию поддаются любые поддающиеся учёту показатели, связанные с работой программы. Например — следующие:
Чистый Python
Начнем тест скорости с кода на чистом Python:
Время выполнения: 52 861 мс, медленнее базовой реализации больше, чем в 1000 раз.
Python интерпретируемый язык, Python использует внутри себя GIL, что делает невозможным параллелизацию на уровне самого кода. Это все очень медленно. Теперь начнем это все ускорять.
Python + Cython
Тут приходит время Cython. Cython это специальный язык, который позволяет внутри обычного Python кода вставлять код на C-образом языке. Далее Cython преобразует это код в .c файлы, которые компилируется в python модули. Эти модули достаточно прозрачно можно вызывать в других частях Python кода. Реализация на Cython выглядит так:
Что тут происходит? На вход функция принимает python numpy объекты, далее они преобразуются в типизированные Cython С-структуры, а далее отключается gil и при помощи специальной конструкции 'prange' внешний цикл выполняется параллельно.
Время выполнения: 76 мс, что в 1.75 раз медленнее, чем базовая реализация.
Ну что, мы почти приблизились к базовой реализации, мы начали писать явный параллельный код. Но ценой этого стал менее читаемый код, мы ушли от чистого Python.
В целом, большинство численных расчетов так и пишутся. Большая часть на NumPy, а некоторые места критичные по быстродействию выносятся в отдельные модули и реализуются на cython.
Способ №2: анализ количества преднамеренных переключений контекста
Проблема с измерением времени, затраченного программой на ожидание чего-либо, заключается в том, что оно, при выполнении разных сеансов измерений для одной и той же программы, может варьироваться из-за чего-то такого, что находится вне сферы действия программы. Иногда DNS-запросы могут оказаться более медленными, чем обычно. Иногда медленнее, чем обычно, могут загружаться какие-то данные. Поэтому полезно было бы использовать некие более предсказуемые показатели, которые не привязаны к скорости работы того, что окружает программу.
Один из способов это сделать заключается в подсчёте того, сколько операций, требующих ожидания, выполнил процесс. То есть — речь идёт о подсчёте количества периодов ожидания, а не времени, которое потрачено на ожидание чего-либо.
Процесс может прекратить пользоваться ресурсами процессора по двум причинам:
- Каждый раз, когда процесс выполняет операцию, которая не завершается мгновенно, например — читает данные из сокета, приостанавливает работу, и так далее, это равносильно тому, что он говорит операционной системе: «Разбуди меня тогда, когда я смогу продолжить работу». Это — так называемое «преднамеренное переключение контекста»: процессор может переключиться на выполнение другого процесса до тех пор, пока в сокете не появятся данные, или до тех пор, пока наш процесс не выйдет из режима ожидания, а также — в других подобных случаях.
- «Непреднамеренное переключение контекста» — это ситуация, в которой операционная система временно останавливает процесс, давая возможность другому процессу воспользоваться ресурсами процессора.
Напишем профилировщик, который подсчитывает преднамеренные переключения контекста с использованием библиотеки psutil :
Теперь снова проведём профилирование кода, который работает с сетью:
Теперь, вместо данных о времени ожидания, мы можем видеть сведения о количестве произошедших преднамеренных переключений контекста.
Обратите внимание на то, что иногда можно видеть преднамеренные переключения контекста в неожиданных местах. Я полагаю, что это происходит тогда, когда из-за ошибок страниц памяти выполняется загрузка данных из файла подкачки.
Читайте также:
- По этой шине сигналы передаются в одном направлении от процессора к оп и устройствам
- Как сделать блок схему в ворде
- Какой серии видеокарта geforce gtx 1050 ti
- Хонда фрид магнитола как записать на жесткий диск
- Используя средства текстового процессора изобразите двоичное дерево соответствующее этому коду