- https://www.peterbaumgartner.com/blog/intro-to-just-enough-cython-to-be-useful/
- https://cython.readthedocs.io/en/latest/src/userguide/external_C_code.html
- https://cython.readthedocs.io/en/latest/src/tutorial/strings.html
Python 3.9.0
python -m venv env
pip install -r requirements.txt
- Сравнение циклов на python и cython
- 1.1 Простой цикл на python с print
- 1.2 Простой цикл на cython с print
- 1.3 Нахождение большой суммы на python
- 1.4 Нахождение большой суммы на cython
- Простое интегрирование
- 2.1 Интегрирование на pandas при помощи python
- 2.2 Компиляция python кода в cython без каких либо изменений
- 2.3 Добавляем в Cython типы данных
- Использования кода на Си в Python
- Запуск llama2.c от karpathy на cython.
Для начала запустим простой скрипт на питоне, который просто будет печатать числа в консоль. (надо же с чего-то начинать)
python loop_python.py
# 1_loop_comparison/loop_python.py
import time
def big_loop():
amount = 10**4 * 5
start = time.time()
for i in range(amount):
print(i)
now = time.time()
total = now - start
print(f"Total loop time = {total}")
if __name__ == "__main__":
big_loop()
Total loop time = 7.097778081893921
Довольно медлено для простого вывода в консоль. Теперь попробуем Cython.
Данная функция также просто печатает числа, получая размер цикла извне.
# 1_loop_comparison/loop_cpython.pyx
from libc.stdio cimport printf
cpdef void big_loop_print(long long amount):
cdef long long i
for i in range(amount):
printf("%lld\n", i)
Теперь чтобы собрать данный код, необходимо запустить следующий скрипт. На самом деле тоже самое можно сделать и через консоль, но мне удобнее держать все в python, так как его удобнее править.
# 1_loop_comparison/setup.py
from distutils.core import setup
from Cython.Build import cythonize
setup(name="loop_cpython", ext_modules=cythonize("./loop_cpython.pyx"))
Выполняем следующую команду, она должна скомпилировать нам бинарник и модуль на python, который мы сможем импртировать в обычном скрипте, при этом сам код будет быстрый как си.
python setup.py build_ext --inplace
Compiling ./loop_cpython.pyx because it changed.
[1/1] Cythonizing ./loop_cpython.pyx
/home/kosenko/miniconda3/lib/python3.10/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: /cephfs/home/kosenko/cython_lesson/1_loop_comparison/loop_cpython.pyx
tree = Parsing.p_module(s, pxd, full_module_name)
Импортируем модуль loop_cpython, который мы скомпилировали ранее.
# 1_loop_comparison/loop_cpython_wrapper.py
from loop_cpython import big_loop_print
import time
def loop_print():
amount = 10**4 * 5
start = time.time()
big_loop_print(amount)
now = time.time()
total = now - start
print(f"Total loop time = {total}")
if __name__ == "__main__":
loop_print()
Запускаем получившуюся программу.
python loop_cpython_wrapper.py
Total loop time = 6.410243034362793
Результат отличается не так сильно :(, получается я вас обманул и технология бесполезна? Нет, просто плохой пример, давайте еще раз.
По настоящему cython начинает показывать себя в вычислительных задачах. Давайте снова попробуем что-то простое, найдем сумму.
# 1_loop_comparison/loop_python.py
import time
def big_loop_sum():
amount = 10**8 * 5
start = time.time()
total_sum = 0
for i in range(amount):
total_sum += i
now = time.time()
total = now - start
print(f"Total Sum = {total_sum} Total loop time = {total}")
if __name__ == "__main__":
big_loop_sum()
python loop_python.py
Total Sum = 124999999750000000 Total loop time = 13.623712062835693
Довольно медленно, фильтры для CV на нем точно лучше не писать. Лучше доверим эту работу cython.
В данной функции я используют тип данных long long, чтобы уж наверняка число поместилось. А вот на python не пришлось бы париться о таких мелочах.
# 1_loop_comparison/loop_cpython.pyx
cpdef long long big_loop_sum(long long amount):
cdef long long total_sum = 0
cdef long long i
for i in range(amount):
total_sum += i
return total_sum
Уже знакомый код сборки.
# 1_loop_comparison/setup.py
from distutils.core import setup
from Cython.Build import cythonize
setup(name="loop_cpython", ext_modules=cythonize("./loop_cpython.pyx"))
Выполняем команду сборки.
python setup.py build_ext --inplace
# 1_loop_comparison/loop_cpython_wrapper.py
from loop_cpython import big_loop_sum
import time
def loop_sum():
amount = 10**8 * 5
start = time.time()
total_sum = big_loop_sum(amount)
now = time.time()
total = now - start
print(f"Total Sum = {total_sum} Total loop time = {total}")
if __name__ == "__main__":
loop_sum()
Запускаем сложные подсчеты...
python loop_cpython_wrapper.py
Total Sum = 124999999750000000 Total loop time = 0.12099981307983398
Погодите, это реально?!
Код сложения стал примерно в ~13.623712062835693/0.12099981307983398 = 112.59 раз быстрее
Да простят меня математики... Интегрирование это короче просто большое суммирование столбиков, а сам интеграл в самом простом понимании означает общую площадь данных столбиков, чем столбиков больше тем выше точность, тем больше вычислений нам требуется.
Довольно теории, переходим к практике!
# 2_pandas_integration/python_pandas.py
import pandas as pd
import numpy as np
import time
# это функция, интеграл которой мы хотим найти
def f(x):
return x * (x - 1)
# это функция интегрирования между двуся чиселками
# а - начало
# b - конец
# N - количество столбиков
# dx - ширина столбика, или если будет угодно шаг
def integrate_f(a, b, N):
s = 0
dx = (b - a) / N
for i in range(N):
s += f(a + i * dx)
result = s * dx
return result
def simple_integration():
amount = 10**4 * 2
df = pd.DataFrame(
{
# генерация рандомных отрицательных чисел
"a": -1 * np.abs(np.random.randn(amount)),
# генерация рандомных положительных чисел
"b": np.abs(np.random.randn(amount)),
# генерация рандомной точности
"N": np.random.randint(100, amount, (amount)),
# это куда мы будем сохранять результат, так надо.
"x": np.float64,
}
)
print(df.head(5))
start = time.time()
# применяем функцию интегрирования для всего датасета
df["x"] = df.apply(lambda x: integrate_f(x["a"], x["b"], x["N"]), axis=1)
total_time = time.time() - start
print(df.head(5))
print(f"Total integration time = {total_time}")
if __name__ == "__main__":
simple_integration()
Запускаем научные вычисления, данной заумной командой.
python python_pandas.py
a b N x
0 -0.895617 0.116314 17235 <class 'numpy.float64'>
1 -1.146209 1.313681 12825 <class 'numpy.float64'>
2 -1.501192 1.131736 3999 <class 'numpy.float64'>
3 -1.123872 0.785894 19235 <class 'numpy.float64'>
4 -0.800014 0.222322 16083 <class 'numpy.float64'>
a b N x
0 -0.895617 0.116314 17235 0.634344
1 -1.146209 1.313681 12825 1.051875
2 -1.501192 1.131736 3999 2.098433
3 -1.123872 0.785894 19235 0.957836
4 -0.800014 0.222322 16083 0.469687
Total integration time = 22.239185571670532
В результате мы получили результат интегрирования для рандомных чисел. Иными словами, мы просто так потратили энергию, вам должно быть стыдно.
Внимательный читатель может заметить, что я ничего не поменял, только названия функций, суть осталась та же.
# 2_pandas_integration/cython_pandas.pyx
def f_plain(x):
return x * (x - 1)
def integrate_f_plain(a, b, N):
s = 0
dx = (b - a) / N
for i in range(N):
s += f_plain(a + i * dx)
return s * dx
Код сборки мало чем отличается, уже скучно...
# 2_pandas_integration/cython_pandas.pyx
from distutils.core import setup
from Cython.Build import cythonize
setup(name="cython_pandas", ext_modules=cythonize("./cython_pandas.pyx"))
Запускаем сборку...
python setup.py build_ext --inplace
Напишем небольшой враппер для нашего модуля, чтобы мы могли его запустить.
# 2_pandas_integration/cython_pandas_wrapper.py
import pandas as pd
import numpy as np
import time
from cython_pandas import integrate_f_plain
def simple_integration():
amount = 10**4 * 2
df = pd.DataFrame(
{
"a": -1 * np.abs(np.random.randn(amount)),
"b": np.abs(np.random.randn(amount)),
"N": np.random.randint(100, amount, (amount)),
"x": np.float64,
}
)
print(df.head(5))
start = time.time()
# код ничем не отличается от предыдущего, только теперь мы используем
# функцию для вычисления, которая была скомпилирована на си
df["x"] = df.apply(lambda x: integrate_f_plain(x["a"], x["b"], x["N"]), axis=1)
total_time = time.time() - start
print(df.head(5))
print(f"Total integration time = {total_time}")
if __name__ == "__main__":
simple_integration()
Запускаем...
python cython_pandas_wrapper.py
a b N x
0 -0.351611 0.798271 8040 <class 'numpy.float64'>
1 -0.278288 1.861562 6194 <class 'numpy.float64'>
2 -0.155229 0.043896 5357 <class 'numpy.float64'>
3 -0.015697 0.648400 7157 <class 'numpy.float64'>
4 -0.728737 0.307716 18081 <class 'numpy.float64'>
a b N x
0 -0.351611 0.798271 8040 -0.072705
1 -0.278288 1.861562 6194 0.463344
2 -0.155229 0.043896 5357 0.012364
3 -0.015697 0.648400 7157 -0.119208
4 -0.728737 0.307716 18081 0.356940
Total integration time = 12.960063934326172
Мы просто скопировали и скомпилировали код, а уже получили прирост в 2х раза!
Заставляет задуматься. Но давайте пойдем дальше и ускорим наш код еще сильнее.
Ранее мы просто добавили python код в cython и скомпилировали его, но смысл Cython заключается не в этом.
# 2_pandas_integration/cython_pandas.pyx
# добавляем типы из языка си, чтобы уже компилятор знал как оптимизировать данный код
# except? означает что cython помечает возникающую ошибку номером -2
# для данной функции мы ожидаем что получим double и вернем double
cdef double f_typed(double x) except? -2:
return x * (x - 1)
# также как и для прошлой, объявляем типы из си
cpdef double integrate_f_typed(double a, double b, int N):
cdef int i
cdef double s, dx
s = 0
dx = (b - a) / N
for i in range(N):
s += f_typed(a + i * dx)
return s * dx
Собираем...
python setup.py build_ext --inplace
Код сборки не поменялся, только теперь все функции оказались типизированы.
# 2_pandas_integration/cython_pandas_wrapper.py
import pandas as pd
import numpy as np
import time
from cython_pandas import integrate_f_plain, integrate_f_typed
def simple_integration_typed():
amount = 10**4 * 2
df = pd.DataFrame(
{
"a": -1 * np.abs(np.random.randn(amount)),
"b": np.abs(np.random.randn(amount)),
"N": np.random.randint(100, amount, (amount)),
"x": np.float64,
}
)
print(df.head(5))
start = time.time()
df["x"] = df.apply(lambda x: integrate_f_typed(x["a"], x["b"], x["N"]), axis=1)
total_time = time.time() - start
print(df.head(5))
print(f"Total integration time = {total_time}")
if __name__ == "__main__":
simple_integration_typed()
python cython_pandas_wrapper.py
a b N x
0 -0.827819 0.130310 17336 <class 'numpy.float64'>
1 -0.223284 0.437670 12644 <class 'numpy.float64'>
2 -0.686149 0.260230 16746 <class 'numpy.float64'>
3 -1.703770 1.133487 9982 <class 'numpy.float64'>
4 -2.181895 0.943481 19789 <class 'numpy.float64'>
a b N x
0 -0.827819 0.130310 17336 0.524032
1 -0.223284 0.437670 12644 -0.039179
2 -0.686149 0.260230 16746 0.315133
3 -1.703770 1.133487 9982 2.943673
4 -2.181895 0.943481 19789 5.678181
Total integration time = 0.2722744941711426
По итогу у нас получилось ускорить изначальную функцию в 22.239185571670532 / 0.2722744941711426 = в 81.6792833988 раз!
И это еще далеко не предел.
На этом моменте стоит остановится и честно признаться вам.. Я говорил правду, но не всю :) Да, Cython помогает ускорить код, но давайте будем благоразумны, даже для простой программы нам пришлось почти полностью переписать программу. А это даже не программа, она умещается на ладони, я говорю про реальные программы.
Так вот, для всяких вычислений(научных) в 2023 году используются другие подходы.
- NumPy - это база, не знать что это позор.
- Numba, она позволяет компилировать код и получать его почти таким же быстрым, но для этого достаточно одного декоратора н-жид, ну то есть @njit
- taichi - самый быстрый в мире компилятор для python, они написали все с нуля и заточили все для скорости. Думаю к ним долгое время ничего не сможет приблизиться, советую попробовать, если вдруг numpy вас чем-то не устроил.
Но понятно что не на только научных вычислениях сошелся мир. Хотя я, честно, долго думал зачем же нужен cython в текущих реалиях. Для себя я нашел только лишь один ответ - написание оберток для c и c++. Cython это клей! Прошу к столу.
Разберем честно украденный пример из официальной документации.
В данном примере мы напишем класс прямоугольника на си, а затем воспользуемся некоторыми методами на python.
Сначала код на с++, питонисты только не пугайтесь.
// 3_import_c++/Rectangle.h
#ifndef RECTANGLE_H
#define RECTANGLE_H
namespace shapes
{
class Rectangle
{
public:
int x0, y0, x1, y1;
// nullable constructor
Rectangle();
// basic constructor
Rectangle(int x0, int y0, int x1, int y1);
// dummy deconstructor
~Rectangle();
void move(int dx, int dy);
void print();
};
}
#endif
// 3_import_c++/Rectangle.cpp
#include <iostream>
#include "Rectangle.h"
namespace shapes
{
// Default constructor
Rectangle::Rectangle() {}
// Overloaded constructor
Rectangle::Rectangle(int x0, int y0, int x1, int y1)
{
this->x0 = x0;
this->y0 = y0;
this->x1 = x1;
this->y1 = y1;
}
// Destructor
Rectangle::~Rectangle()
{
printf("Object deconstruction....\n");
}
// Move the rectangle by dx dy
void Rectangle::move(int dx, int dy)
{
printf("Moving...\n");
this->x0 += dx;
this->y0 += dy;
this->x1 += dx;
this->y1 += dy;
}
// функция печати, добавил для наглядности, что мы можем что-то печатать из си кода
void Rectangle::print()
{
printf("x0=%d y0=%d x1=%d y1=%d\n", this->x0, this->y0, this->x1, this->y1);
}
}
Ну что сказать. Простейший класс на с++, который содержит в себе какую-то логику работы с прямоугольником. Теперь попробуем импортировать это в python.
# 3_import_c++/rectangle_cpy.pyx
# distutils: language = c++ <---- данная строчка должна быть в начале файла,
# только так компилятор поймет что ему нужно работать с с++ кодом
# данная строчка говорит компилятору cython чтобы он нашел данный файл
# и скомпилировал его. Это нужно чтобы мы потом могли вызвать
# необходимую нам функцию, описанную в данном файле.
cdef extern from "Rectangle.cpp":
pass
# объявляем еще раз класс, только на cython
# это необходимо чтобы он потом смог найти и сопоставить
# нужные нам поля. согласен некоторый бойлерплейт получается,
# но кажется это довольно небольшая цена
cdef extern from "Rectangle.h" namespace "shapes":
cdef cppclass Rectangle:
int x0, y0, x1, y1
Rectangle() except +
Rectangle(int, int, int, int) except +
void move(int, int)
void print()
# До этого мы работали исключительно с классами на с++, чтобы
# python нас вообще понял нужно создать еще одну обертку(да, как-то многовато уже)
# данная обертка, к примеру, может вынести только нужные нам поля и функции
cdef class PyRectangle:
cdef Rectangle *c_rect
# согласно документации __cinit__ это единственное надежное место, где мы
# можем объвлять переменные ссылочного типа
def __cinit__(self):
self.c_rect = new Rectangle()
# а вот это уже специальный деконструктор, который управляет памятью
# именно он определяет как уничтожится объект и очиститься память
def __dealloc__(self):
del self.c_rect
def __init__(self, int x0, int y0, int x1, int y1):
self.c_rect.x0 = x0
self.c_rect.y0 = y0
self.c_rect.x1 = x1
self.c_rect.y1 = y1
# простые обертки
def print(self):
self.c_rect.print()
def move(self, dx, dy):
self.c_rect.move(dx, dy)
# Attribute access
@property
def x0(self):
return self.c_rect.x0
@x0.setter
def x0(self, x0):
self.c_rect.x0 = x0
# Attribute access
@property
def x1(self):
return self.c_rect.x1
@x1.setter
def x1(self, x1):
self.c_rect.x1 = x1
# Attribute access
@property
def y0(self):
return self.c_rect.y0
@y0.setter
def y0(self, y0):
self.c_rect.y0 = y0
# Attribute access
@property
def y1(self):
return self.c_rect.y1
@y1.setter
def y1(self, y1):
self.c_rect.y1 = y1
Компилируем...
python setup.py build_ext --inplace
Функция управления данным кодом.
# 3_import_c++/run_example.py
from rectangle_cpy import PyRectangle
if __name__ == "__main__":
rectangle = PyRectangle(1, 20, 3, 43)
rectangle.print()
print("Setting y1")
rectangle.y1 = 12
rectangle.print()
rectangle.move(1, 4)
rectangle.print()
python run_example.py
x0=1 y0=20 x1=3 y1=43
Setting y1
x0=1 y0=20 x1=3 y1=12
Moving...
x0=2 y0=24 x1=4 y1=16
Object deconstruction....
Ценность данного примера в том, что мы теперь можем писать всякую крутую фигню на с++, а затем элегантно импортировать ее в python.
Но довольно академических сферических коней в вакууме. Давайте применим cython по назначению!
Представим ситуацию. Перед вами черный ящик. Вы не понимаете его структуру, вы не знаете что это такое. Известно лишь одно, чем сильнее вы бъете по этому ящику, тем больше денег из него вываливается. В какой-то момент вам надоело подставлять ладони и захотелось как-то выстроить инфраструкту вокруг этого полезного артефакта.
Так вот это как си бинарники выглядят для питонистов.
В данном случае мы имеем маленькую языковую модель, которая генерирует нам какой-то текст. Данная реализация включает в себя просто вывод чего-то в консоль, а мы хотим как-то перехватить данный текст, может быть даже как-то модифицировать.
Приступим.
Чтобы посмотреть как работает оригинальная модель выполним следующую команду. кто не на linux, соболезную.
make run
./run stories15M.bin
<s>
Once upon a time, there was a little girl named Lily. She loved to play with her toys and eat cookies. One day, she went to the park to play with her friends. While she was playing, she saw a woman who looked very wealthy. She asked her mom what that meant, and her mom explained that the woman was wearing a nice coat.
Lily was curious, and asked if she could go say hello. She waved at the woman and said hello. The woman smiled and said hi back, and Lily was so happy. They played together for a while and had fun.
Suddenly, the woman's cat jumped on Lily's shoulder and surprised her. Lily laughed and laughed, and the woman laughed too. They became friends and played together all day. From that day on, Lily loved visiting people with big smiles and enthusiasm.
<s>
One day, a little girl named Amy went to her wardrobe to find her favorite dress. She opened the wardrobe and saw many things she had never seen before. She saw a shirt, a sock, and a toy. Amy felt very fine because she was having so much fun
achieved tok/s: 65.891473
Великолепно, оно генерирует текст. Но мы ничего не можем сделать с этим текстом. Давайте обернем код и посмотрим что будет.
Сначала просто запустим данную программу из python.
Для этого объявим несколько функций.
#ifndef LLAMA2_H
#define LLAMA2_H
// просто вызывает оригинальный код
void generate_1(char *checkpoint);
#endif
// 4_import_llama/llama_c.c
/**
* ДОФИГА УМНОГО КОДА ВЫШЕ
*/
void generate_1(char *checkpoint)
{
// дефолтная функция генерации без изменений, только мы передаем туда название файла чекпоинта
basic_generation_1(checkpoint);
}
Теперь как-то впихнем написанное в наш cython.
# 4_import_llama/llama_cy.pyx
cdef extern from "llama_c.h":
void generate_1(char* checkpoint)
def llama_generate_1(checkpoint: str = 'model.bin'):
# передаем из обычного питона строку
# затем кодируем ее в байты
c_name = checkpoint.encode('utf-8')
# создаем переменную ссылочного типа
cdef char* c_checkpoint = c_name
# и наконец вызываем нужную функцию генерации
generate_1(c_checkpoint)
Собираем...
# 4_import_llama/setup.py
from setuptools import setup
from Cython.Build import cythonize
from distutils.extension import Extension
extensions = [Extension("llama_cy", ["llama_cy.pyx", "llama_c.c"])]
setup(
ext_modules=extensions,
extra_compile_args=["-O3"],
)
python setup.py build_ext --inplace
Теперь для запуска нашей нейронки воспользуемся кодом на python.
# 4_import_llama/run_llama.py
from llama_cy import llama_generate_1
if __name__ == "__main__":
llama_generate_1("stories15M.bin")
На выходе мы получили обертку над си кодом, который можем запускать сколько нашей душе угодно. Но зачем...
Теперь попробуем получить чуть больше полезной нагрузки от данной черной коробки.
Для этого нам придется немного переписать наш код на си (о ужас). Сразу глубоко извиняюсь перед сишниками, так как ничего более сложного чем little c на этом языке я не разбирал и не кодил.
Так вот, по факту нам нужно создать массив строк, затем вычленить токен из генерации, добавить в массив ну и собственно вернуть данную ссылку на этот массив.
Так уж и быть теперь я приведу полный листинг функции.
// 4_import_llama/llama_c.c
char *basic_generation_2(char *checkpoint)
{
// poor man's C argparse
// char *checkpoint = NULL; // e.g. out/model.bin
float temperature = 0.9f; // e.g. 1.0, or 0.0
int steps = 256; // max number of steps to run for, 0: use seq_len
char *prompt = NULL; // prompt string
// ОБЪЯВЛЯЕМ МАССИВ МАССИВОВ
// ----------
char **generation_result = (char **)malloc(sizeof(char *) * steps);
// ----------
// 'checkpoint' is necessary arg
if (!checkpoint)
{
printf("You should provide some checkpoint\n");
return 1;
}
// seed rng with time. if you want deterministic behavior use temperature 0.0
rng_seed = (unsigned int)time(NULL);
// read in the model.bin file
Config config;
TransformerWeights weights;
int fd = 0; // file descriptor for memory mapping
float *data = NULL; // memory mapped data pointer
long file_size; // size of the checkpoint file in bytes
{
FILE *file = fopen(checkpoint, "rb");
if (!file)
{
printf("Couldn't open file %s\n", checkpoint);
return 1;
}
// read in the config header
if (fread(&config, sizeof(Config), 1, file) != 1)
{
return 1;
}
// negative vocab size is hacky way of signaling unshared weights. bit yikes.
int shared_weights = config.vocab_size > 0 ? 1 : 0;
config.vocab_size = abs(config.vocab_size);
// figure out the file size
fseek(file, 0, SEEK_END); // move file pointer to end of file
file_size = ftell(file); // get the file size, in bytes
fclose(file);
// memory map the Transformer weights into the data pointer
fd = open(checkpoint, O_RDONLY); // open in read only mode
if (fd == -1)
{
printf("open failed!\n");
return 1;
}
data = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == MAP_FAILED)
{
printf("mmap failed!\n");
return 1;
}
float *weights_ptr = data + sizeof(Config) / sizeof(float);
checkpoint_init_weights(&weights, &config, weights_ptr, shared_weights);
}
// right now we cannot run for more than config.seq_len steps
if (steps <= 0 || steps > config.seq_len)
{
steps = config.seq_len;
}
// read in the tokenizer.bin file
char **vocab = (char **)malloc(config.vocab_size * sizeof(char *));
float *vocab_scores = (float *)malloc(config.vocab_size * sizeof(float));
unsigned int max_token_length;
{
FILE *file = fopen("tokenizer.bin", "rb");
if (!file)
{
printf("couldn't load tokenizer.bin\n");
return 1;
}
if (fread(&max_token_length, sizeof(int), 1, file) != 1)
{
printf("failed read\n");
return 1;
}
int len;
for (int i = 0; i < config.vocab_size; i++)
{
if (fread(vocab_scores + i, sizeof(float), 1, file) != 1)
{
printf("failed read\n");
return 1;
}
if (fread(&len, sizeof(int), 1, file) != 1)
{
printf("failed read\n");
return 1;
}
vocab[i] = (char *)malloc(len + 1);
if (fread(vocab[i], len, 1, file) != 1)
{
printf("failed read\n");
return 1;
}
vocab[i][len] = '\0'; // add the string terminating token
}
fclose(file);
}
// create and init the application RunState
RunState state;
malloc_run_state(&state, &config);
// process the prompt, if any
int *prompt_tokens = NULL;
int num_prompt_tokens = 0;
if (prompt != NULL)
{
prompt_tokens = (int *)malloc(config.seq_len * sizeof(int));
bpe_encode(prompt, vocab, vocab_scores, config.vocab_size, max_token_length, prompt_tokens, &num_prompt_tokens);
}
// start the main loop
long start = 0; // used to time our code, only initialized after first iteration
int next; // will store the next token in the sequence
int token = 1; // init with token 1 (=BOS), as done in Llama-2 sentencepiece tokenizer
int pos = 0; // position in the sequence
// printf("<s>\n"); // explicit print the initial BOS token for stylistic symmetry reasons
while (pos < steps)
{
// forward the transformer to get logits for the next token
transformer(token, pos, &config, &state, &weights);
if (pos < num_prompt_tokens)
{
// if we are still processing the input prompt, force the next prompt token
next = prompt_tokens[pos];
}
else
{
// sample the next token
if (temperature == 0.0f)
{
// greedy argmax sampling: take the token with the highest probability
next = argmax(state.logits, config.vocab_size);
}
else
{
// apply the temperature to the logits
for (int q = 0; q < config.vocab_size; q++)
{
state.logits[q] /= temperature;
}
// apply softmax to the logits to get the probabilities for next token
softmax(state.logits, config.vocab_size);
// we sample from this distribution to get the next token
next = sample(state.logits, config.vocab_size);
}
}
// following BOS token (1), sentencepiece decoder strips any leading whitespace (see PR #89)
char *token_str = (token == 1 && vocab[next][0] == ' ') ? vocab[next] + 1 : vocab[next];
// СОХРАНЯЕМ СГЕНЕРИРОВАННЫЙ ТОКЕН
// -------------
generation_result[pos] = token_str;
// -------------
// advance forward
token = next;
pos++;
// init our timer here because the first iteration is slow due to memmap
if (start == 0)
{
start = time_in_ms();
}
}
// report achieved tok/s
long end = time_in_ms();
// printf("\nachieved tok/s: %f\n", (steps - 1) / (double)(end - start) * 1000);
// ПОЛУЧАЕМ КОНЕЧНУЮ СТРОКУ ГЕНЕРАЦИИ
char *result = malloc(sizeof(char) * 3000);
for (int i = 0; i < steps; i++)
{
strcat(result, generation_result[i]);
}
// memory and file handles cleanup
free_run_state(&state);
for (int i = 0; i < config.vocab_size; i++)
{
free(vocab[i]);
}
free(vocab);
free(vocab_scores);
if (prompt_tokens != NULL)
free(prompt_tokens);
if (data != MAP_FAILED)
munmap(data, file_size);
if (fd != -1)
close(fd);
// ВОЗВРАЩАЕМ РЕЗУЛЬТАТ, НЕВЕРОЯТНОООООО
return result;
}
char *generate_2(char *checkpoint)
{
char *generation_result = (char *)basic_generation_2(checkpoint);
return generation_result;
}
Обновляем файл с заголовками.
#ifndef LLAMA2_H
#define LLAMA2_H
void generate_1(char *checkpoint);
char* generate_2(char *checkpoint);
#endif
Обновляем файл с вызовом генерации.
# 4_import_llama/llama_cy.pyx
cdef extern from "llama_c.h":
void generate_1(char* checkpoint)
char* generate_2(char* checkpoint)
def llama_generate_2(checkpoint: str = 'model.bin'):
# pass checkpoint name to our c code and get result
c_name = checkpoint.encode('utf-8')
cdef char* c_checkpoint = c_name
cdef char* generation_result = generate_2(c_checkpoint)
result = generation_result.decode('unicode_escape')
print(result)
Собираем ...
python setup.py build_ext --inplace
# 4_import_llama/run_llama.py
from llama_cy import llama_generate_2
if __name__ == "__main__":
llama_generate_2("stories15M.bin")
По итогу мы получили текст из си, на python.
°Ó`\ÚOnce upon a time, there was a big, red motor. It liked to run and play in the park. The motor was very slow, but it was a happy motor.
One day, the motor saw a little boy. The boy wanted to play with the motor, but it didn't move. The motor tried to explain, "The boy needs to walk, and try again." The boy was scared to walk, but the motor said, "It's okay, we can play together."
So the boy walked with the motor. They had fun running and playing in the park. But then, something unexpected happened. The motor started to slow down! The boy looked around and saw that the motor was not in a funny way anymore.
They were both scared, but then they saw a big, friendly bear. The bear surprised them and they laughed. The boy forgot he was scared of the motor now. From that day on, the boy, the motor, and the bear became best friends. They played together in the park every day, and the boy never thought the motor could be so much fun.
<s>
Once upon a time, there was a little boy named Timmy. He loved to play with his toy cars
Был ли более простой способ? Ну типа был. Можно было вызвать в фоне программу и получить ее output. Просто хотелось показать более менее реальный пример связки программы на си и python.
Спасибо за внимание. Меня можно найти тут канал телеграм