<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>Victor</title><author><name>Victor</name></author><id>https://teletype.in/atom/avenida</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/avenida?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@avenida?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=avenida"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/avenida?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-04-16T11:46:38.578Z</updated><entry><id>avenida:bGx_rXp-zSv</id><link rel="alternate" type="text/html" href="https://teletype.in/@avenida/bGx_rXp-zSv?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=avenida"></link><title>16-, 8- и 4-битные форматы чисел с плавающей запятой</title><published>2023-12-08T08:40:25.489Z</published><updated>2023-12-08T08:40:25.489Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img4.teletype.in/files/f5/8a/f58ac37f-025a-4336-93e0-5ab3d0f7965b.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/c3f/bad/c83/c3fbadc838abfa6ca95c601ca774109d.png&quot;&gt;https://habr.com/ru/companies/wunderfund/articles/776496/</summary><content type="html">
  &lt;p id=&quot;KrX8&quot;&gt;&lt;a href=&quot;https://habr.com/ru/companies/wunderfund/articles/776496/&quot; target=&quot;_blank&quot;&gt;https://habr.com/ru/companies/wunderfund/articles/776496/&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;cT7b&quot;&gt;Средний&lt;/p&gt;
  &lt;p id=&quot;XBGx&quot;&gt;15 мин&lt;/p&gt;
  &lt;p id=&quot;nzAq&quot;&gt;16K &lt;a href=&quot;https://habr.com/ru/companies/wunderfund/articles/&quot; target=&quot;_blank&quot;&gt;Блог компании Wunder Fund&lt;/a&gt;&lt;a href=&quot;https://habr.com/ru/hubs/webdev/&quot; target=&quot;_blank&quot;&gt;Веб-разработка*&lt;/a&gt;&lt;a href=&quot;https://habr.com/ru/hubs/python/&quot; target=&quot;_blank&quot;&gt;Python*&lt;/a&gt;&lt;a href=&quot;https://habr.com/ru/hubs/programming/&quot; target=&quot;_blank&quot;&gt;Программирование*&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;H0Mo&quot;&gt;Уже лет 50, со времён выхода первого издания «Языка программирования Си» Кернигана и Ритчи, известно, что «числа с плавающей запятой» одинарной точности имеют размер 32 бита, а числа двойной точности — 64 бита. Существуют ещё и 80-битные числа расширенной точности типа «long double». Эти типы данных покрывали почти все нужды обработки вещественных чисел. Но в последние несколько лет, с наступлением эпохи больших нейросетевых моделей, у разработчиков появилась потребность в типах данных, которые не «больше», а «меньше» существующих, потребность в том, чтобы как можно сильнее «сжать» типы данных, представляющие числа с плавающей запятой.&lt;/p&gt;
  &lt;figure id=&quot;khPs&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/c3f/bad/c83/c3fbadc838abfa6ca95c601ca774109d.png&quot; width=&quot;780&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;EpzC&quot;&gt;Я, честно говоря, был удивлён, когда узнал о существовании 4-битного формата для представления чисел с плавающей запятой. Да как такое вообще возможно? Лучший способ узнать об этом — самостоятельно поработать с такими числами. Сейчас мы исследуем самые популярные форматы чисел с плавающей запятой, создадим с использованием некоторых из них простую нейронную сеть и понаблюдаем за тем, как она работает.&lt;/p&gt;
  &lt;h2 id=&quot;7Zwc&quot;&gt;«Стандартные» 32-битные числа с плавающей запятой&lt;/h2&gt;
  &lt;p id=&quot;KJcv&quot;&gt;Прежде чем переходить к описанию «экстремальных» типов данных — давайте вспомним о стандартном типе. Стандарт &lt;a href=&quot;https://en.wikipedia.org/wiki/IEEE_754&quot; target=&quot;_blank&quot;&gt;IEEE 754&lt;/a&gt;, регламентирующий арифметику с плавающей запятой, был принят в 1985 году Институтом инженеров электротехники и электроники (Institute of Electrical and Electronics Engineers, IEEE). Типичное 32-битное число с плавающей запятой, в соответствии с этим стандартном, выглядит так:&lt;/p&gt;
  &lt;figure id=&quot;ygse&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/6ab/49e/1a6/6ab49e1a68d008f53d49cec5e3d7ee06.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Пример 32-битного числа с плавающей запятой (&lt;a href=&quot;https://en.wikipedia.org/wiki/IEEE_754&quot; target=&quot;_blank&quot;&gt;источник&lt;/a&gt;)&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Fxvm&quot;&gt;Первый бит задаёт знак числа, следующие 8 битов представляют порядок, а остальные биты — мантиссу. Десятичное значение числа находят по следующей формуле:&lt;/p&gt;
  &lt;figure id=&quot;tKqR&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/699/26b/ccc/69926bcccaf2ada34c1e2830eee78b1a.png&quot; width=&quot;370&quot; /&gt;
    &lt;figcaption&gt;Формула для нахождения десятичного значения двоичного числа с плавающей запятой (&lt;a href=&quot;https://en.wikipedia.org/wiki/Floating-point_arithmetic&quot; target=&quot;_blank&quot;&gt;источник&lt;/a&gt;)&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;sCNn&quot;&gt;Вот — простая вспомогательная функция, которая позволит нам выводить на экран числа с плавающей запятой в их двоичном виде:&lt;/p&gt;
  &lt;pre id=&quot;EvLD&quot;&gt;import struct

def print_float32(val: float):
    &amp;quot;&amp;quot;&amp;quot; Print Float32 in a binary form &amp;quot;&amp;quot;&amp;quot;
    m = struct.unpack(&amp;#x27;I&amp;#x27;, struct.pack(&amp;#x27;f&amp;#x27;, val))[0]
    return format(m, &amp;#x27;b&amp;#x27;).zfill(32)

print_float32(0.15625)

# &amp;gt; 00111110001000000000000000000000&lt;/pre&gt;
  &lt;p id=&quot;Btkz&quot;&gt;Напишем ещё одну вспомогательную функцию, которая позволяет выполнять обратное преобразование. Позже она нам пригодится:&lt;/p&gt;
  &lt;pre id=&quot;QQao&quot;&gt;def ieee_754_conversion(sign, exponent_raw, mantissa, exp_len=8, mant_len=23):
    &amp;quot;&amp;quot;&amp;quot; Convert binary data into the floating point value &amp;quot;&amp;quot;&amp;quot;
    sign_mult = -1 if sign == 1 else 1
    exponent = exponent_raw - (2 ** (exp_len - 1) - 1)
    mant_mult = 1
    for b in range(mant_len - 1, -1, -1):
        if mantissa &amp;amp; (2 ** b):
            mant_mult += 1 / (2 ** (mant_len - b))

    return sign_mult * (2 ** exponent) * mant_mult


ieee_754_conversion(0b0, 0b01111100, 0b01000000000000000000000)

#&amp;gt; 0.15625&lt;/pre&gt;
  &lt;p id=&quot;T4RO&quot;&gt;И я надеюсь, что все программисты и IT‑энтузиасты знают, что точность чисел с плавающей запятой ограничена:&lt;/p&gt;
  &lt;pre id=&quot;nKjK&quot;&gt;val = 3.14
print(f&amp;quot;{val:.20f}&amp;quot;)

# &amp;gt; 3.14000000000000012434&lt;/pre&gt;
  &lt;p id=&quot;MyrH&quot;&gt;Это, в данном случае, не такая уж и проблема. Но, чем меньше у нас бит, тем меньше точность, на которую можно рассчитывать. И, как мы скоро увидим, точность вполне может быть проблемой. А теперь — начнём путешествие по кроличьей норе…&lt;/p&gt;
  &lt;h2 id=&quot;UMVs&quot;&gt;16-битные числа с плавающей запятой&lt;/h2&gt;
  &lt;p id=&quot;YbDI&quot;&gt;Очевидно, раньше особой потребности в 16-битных числах с плавающей запятой не было, поэтому описание соответствующего типа было добавлено в стандарт IEEE 754 только в 2008 году. У таких чисел имеется знаковый бит, 5-битный порядок и 10-битная мантисса:&lt;/p&gt;
  &lt;figure id=&quot;1gBQ&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/d2d/679/d61/d2d679d618d217e44262a8b6dd50cdc1.png&quot; width=&quot;609&quot; /&gt;
    &lt;figcaption&gt;Пример 16-битного числа с плавающей запятой (&lt;a href=&quot;https://en.wikipedia.org/wiki/Bfloat16_floating-point_format&quot; target=&quot;_blank&quot;&gt;источник&lt;/a&gt;)&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;PEkt&quot;&gt;Логика преобразования десятичных представлений таких чисел в двоичные точно такая же, как и при работе с 32-битными числами, но их точность, безусловно, ниже, чем у 32-битных чисел. Выведем 16-битное число с плавающей запятой в двоичном виде:&lt;/p&gt;
  &lt;pre id=&quot;gZmZ&quot;&gt;import numpy as np

def print_float16(val: float):
    &amp;quot;&amp;quot;&amp;quot; Print Float16 in a binary form &amp;quot;&amp;quot;&amp;quot;
    m = struct.unpack(&amp;#x27;H&amp;#x27;, struct.pack(&amp;#x27;e&amp;#x27;, np.float16(val)))[0]
    return format(m, &amp;#x27;b&amp;#x27;).zfill(16)

print_float16(3.14)

# &amp;gt; 0100001001001000&lt;/pre&gt;
  &lt;p id=&quot;JzvE&quot;&gt;Прибегнув к методу, которым мы уже пользовались, можем выполнить обратное преобразование:&lt;/p&gt;
  &lt;pre id=&quot;cERq&quot;&gt;ieee_754_conversion(0, 0b10000, 0b1001001000, exp_len=5, mant_len=10)

# &amp;gt; 3.140625&lt;/pre&gt;
  &lt;p id=&quot;KuTs&quot;&gt;А вот как можно найти максимальное значение, представимое в виде числа типа &lt;code&gt;float16&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;d0tG&quot;&gt;ieee_754_conversion(0, 0b11110, 0b1111111111, exp_len=5, mant_len=10)

#&amp;gt; 65504.0&lt;/pre&gt;
  &lt;p id=&quot;J4zc&quot;&gt;Я использовал тут &lt;code&gt;0b11110&lt;/code&gt; из-за того, что в стандарте IEEE 754 число &lt;code&gt;0b11111&lt;/code&gt; зарезервировано для «бесконечности». Можно найти и возможное минимальное значение:&lt;/p&gt;
  &lt;pre id=&quot;ibGr&quot;&gt;ieee_754_conversion(0, 0b00001, 0b0000000000, exp_len=5, mant_len=10)

#&amp;gt; 0.00006104&lt;/pre&gt;
  &lt;p id=&quot;qyKr&quot;&gt;Для большинства разработчиков типы, вроде описанного — это «неизведанная территория». И, судя по всему, даже в наши дни в C++ нет стандартного 16-битного типа данных для чисел с плавающей запятой. Но разнообразие типов этим не ограничивается.&lt;/p&gt;
  &lt;h2 id=&quot;bjuT&quot;&gt;16-битные числа с плавающей запятой «bfloat» (BFP16)&lt;/h2&gt;
  &lt;p id=&quot;avS3&quot;&gt;Этот формат чисел с плавающей запятой разработан командой Google Brain. Он спроектирован специально для нужд машинного обучения (буква «B» в его названии — это сокращение от «brain»). Это — модификация «стандартного» 16-битного формата: порядок увеличен до 8 бит, в результате диапазон значений &lt;code&gt;bfloat16&lt;/code&gt;, на самом деле, получается таким же, как у &lt;code&gt;float32&lt;/code&gt;. Но размер мантиссы был уменьшен до 7 бит:&lt;/p&gt;
  &lt;figure id=&quot;DRBI&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/dfb/7f4/b53/dfb7f4b531f5bf6f02aaf8299026c618.png&quot; width=&quot;609&quot; /&gt;
    &lt;figcaption&gt;Пример 16-битного числа с плавающей запятой bfloat16 (&lt;a href=&quot;https://en.wikipedia.org/wiki/Bfloat16_floating-point_format&quot; target=&quot;_blank&quot;&gt;источник&lt;/a&gt;)&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;IS7i&quot;&gt;Проведём небольшой эксперимент, аналогичный предыдущим:&lt;/p&gt;
  &lt;pre id=&quot;wYj0&quot;&gt;ieee_754_conversion(0, 0b10000000, 0b1001001, exp_len=8, mant_len=7)

#&amp;gt; 3.140625&lt;/pre&gt;
  &lt;p id=&quot;NUtB&quot;&gt;Как уже было сказано — из‑за увеличенного порядка формат &lt;code&gt;bfloat16&lt;/code&gt; вмещает в себя гораздо больший диапазон значений, чем &lt;code&gt;float16&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;bJ86&quot;&gt;ieee_754_conversion(0, 0b11111110, 0b1111111, exp_len=8, mant_len=7)

#&amp;gt; 3.3895313892515355e+38&lt;/pre&gt;
  &lt;p id=&quot;4tgD&quot;&gt;Это — гораздо лучше в сравнении с &lt;code&gt;65504.0&lt;/code&gt; из предыдущего примера, но, как уже было сказано, точность чисел &lt;code&gt;bfloat16&lt;/code&gt; ниже из‑за того, что на мантиссу приходится меньшее число бит. Можно протестировать оба типа в TensorFlow:&lt;/p&gt;
  &lt;pre id=&quot;YS7Z&quot;&gt;import tensorflow as tf

print(f&amp;quot;{tf.constant(1.2, dtype=tf.float16).numpy().item():.12f}&amp;quot;)

# &amp;gt; 1.200195312500

print(f&amp;quot;{tf.constant(1.2, dtype=tf.bfloat16).numpy().item():.12f}&amp;quot;)

# &amp;gt; 1.203125000000&lt;/pre&gt;
  &lt;h2 id=&quot;eNoW&quot;&gt;8-битные числа с плавающей запятой (FP8)&lt;/h2&gt;
  &lt;p id=&quot;l0gD&quot;&gt;Этот (сравнительно новый) формат был предложен в 2022 году и, как может догадаться читатель, он тоже создан для целей машинного обучения. Модели становятся всё больше и больше, их всё сложнее и сложнее умещать в памяти GPU. Формат FP8 существует в двух вариантах: E4M3 (4-битный порядок и 3-битная мантисса) и E5M2 (5-битный порядок и 2-битная мантисса):&lt;/p&gt;
  &lt;figure id=&quot;ORgd&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/0b1/dbb/d22/0b1dbbd22150843b54a5b1ec4562f9e2.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Пример 8-битных чисел с плавающей запятой (&lt;a href=&quot;https://en.wikipedia.org/wiki/Minifloat&quot; target=&quot;_blank&quot;&gt;источник&lt;/a&gt;)&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;I3tU&quot;&gt;Выясним максимально возможные значения чисел для обоих вариантов FP8:&lt;/p&gt;
  &lt;pre id=&quot;NyiC&quot;&gt;ieee_754_conversion(0, 0b1111, 0b110, exp_len=4, mant_len=3)

# &amp;gt; 448.0

ieee_754_conversion(0, 0b11110, 0b11, exp_len=5, mant_len=2)

# &amp;gt; 57344.0&lt;/pre&gt;
  &lt;p id=&quot;SkI5&quot;&gt;Формат FP8 можно использовать и в TensorFlow:&lt;/p&gt;
  &lt;pre id=&quot;AclB&quot;&gt;import tensorflow as tf
from tensorflow.python.framework import dtypes


a_fp8 = tf.constant(3.14, dtype=dtypes.float8_e4m3fn)
print(a_fp8)

# &amp;gt; 3.25

a_fp8 = tf.constant(3.14, dtype=dtypes.float8_e5m2)
print(a_fp8)

# &amp;gt; 3.0&lt;/pre&gt;
  &lt;p id=&quot;HfBj&quot;&gt;Нарисуем график синуса, используя оба типа:&lt;/p&gt;
  &lt;pre id=&quot;v1ma&quot;&gt;import numpy as np
import tensorflow as tf
from tensorflow.python.framework import dtypes
import matplotlib.pyplot as plt

length = np.pi * 4
resolution = 200
xvals = np.arange(0, length, length / resolution)
wave = np.sin(xvals)
wave_fp8_1 = tf.cast(wave, dtypes.float8_e4m3fn)
wave_fp8_2 = tf.cast(wave, dtypes.float8_e5m2)

plt.rcParams[&amp;quot;figure.figsize&amp;quot;] = (14, 5)
plt.plot(xvals, wave_fp8_1.numpy())
plt.plot(xvals, wave_fp8_2.numpy())
plt.show()&lt;/pre&gt;
  &lt;p id=&quot;jvL0&quot;&gt;Результат, что удивительно, не так уж и плох:&lt;/p&gt;
  &lt;figure id=&quot;Ke8K&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/2ce/3eb/306/2ce3eb3061c1fd7b6091a5a94a671963.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Синусоидальная волна, построенная по данным, представленным в разных вариантах формата FP8 (изображение подготовлено автором)&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;a2uA&quot;&gt;Тут ясно видны некоторые потери точности, но то, что получилось, очень даже похоже на синусоиду!&lt;/p&gt;
  &lt;h2 id=&quot;IfM9&quot;&gt;4-битные числа с плавающей запятой (FP4, NF4)&lt;/h2&gt;
  &lt;p id=&quot;0MXv&quot;&gt;А теперь перейдём к самой «безумной» теме — к 4-битным числам с плавающей запятой (FP4). На самом деле такие числа — это самые компактные значения с плавающей запятой, соответствующие стандарту IEEE, имеющие 1 бит на знак, 2 бита на порядок и 1 бит на мантиссу:&lt;/p&gt;
  &lt;figure id=&quot;S46A&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/bf3/d1f/be5/bf3d1fbe54f3e486040f46e75c87e666.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Пример значения FP4 (изображение подготовлено автором)&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;CjyI&quot;&gt;Количество значений, которые можно сохранить в формате FP4, невелико. Все эти значения, на самом деле, помещаются в массив на 16 элементов!&lt;/p&gt;
  &lt;p id=&quot;DdVn&quot;&gt;Ещё одна возможная реализация 4-битных чисел с плавающей запятой представлена типом данных, называемым NormalFloat (NF4). Значения NF4 оптимизированы для сохранения нормально распределённых данных. Все возможные значения NF4 легко вывести на экран в виде небольшого списка (при исследовании других типов данных это может оказаться совсем непростой задачей):&lt;/p&gt;
  &lt;pre id=&quot;SesK&quot;&gt;[-1.0, -0.6961928009986877, -0.5250730514526367, -0.39491748809814453, 
 -0.28444138169288635, -0.18477343022823334, -0.09105003625154495, 0.0,
  0.07958029955625534, 0.16093020141124725, 0.24611230194568634, 0.33791524171829224, 
  0.44070982933044434, 0.5626170039176941, 0.7229568362236023, 1.0]&lt;/pre&gt;
  &lt;p id=&quot;IjYN&quot;&gt;И тип FP4, и тип NF4 реализованы в Python‑библиотеке &lt;a href=&quot;https://github.com/TimDettmers/bitsandbytes&quot; target=&quot;_blank&quot;&gt;bitsandbytes&lt;/a&gt;. Давайте, в качестве примера, преобразуем массив &lt;code&gt;[1.0, 2.0, 3.0, 4.0]&lt;/code&gt; в формат FP4:&lt;/p&gt;
  &lt;pre id=&quot;HjHY&quot;&gt;from bitsandbytes import functional as bf

def print_uint(val: int, n_digits=8) -&amp;gt; str:
    &amp;quot;&amp;quot;&amp;quot; Convert 42 =&amp;gt; �&amp;#x27; &amp;quot;&amp;quot;&amp;quot;
    return format(val, &amp;#x27;b&amp;#x27;).zfill(n_digits)

device = torch.device(&amp;quot;cuda&amp;quot;)
x = torch.tensor([1.0, 2.0, 3.0, 4.0], device=device)
x_4bit, qstate = bf.quantize_fp4(x, blocksize=64)

print(x_4bit)
# &amp;gt; tensor([[117], [35]], dtype=torch.uint8)

print_uint(x_4bit[0].item())
# &amp;gt; 01110101
print_uint(x_4bit[1].item())
# &amp;gt; 00100011

print(qstate)
# &amp;gt; (tensor([4.]), 
# &amp;gt;  &amp;#x27;fp4&amp;#x27;, 
# &amp;gt;  tensor([ 0.0000,  0.0052,  0.6667,  1.0000,  0.3333,  0.5000,  0.1667,  0.2500,
# &amp;gt;           0.0000, -0.0052, -0.6667, -1.0000, -0.3333, -0.5000, -0.1667, -0.2500])])&lt;/pre&gt;
  &lt;p id=&quot;spN5&quot;&gt;Результат выглядит интересно. На выходе получилось два объекта: 16-битный массив &lt;code&gt;[117, 35]&lt;/code&gt;, содержащий наши 4 числа, и объект «состояния», в котором находятся коэффициент масштабирования 4.0 и тензор со всеми шестнадцатью FP4-числами.&lt;/p&gt;
  &lt;p id=&quot;gist&quot;&gt;Например, первое 4-битное число — это «0111» (=7). В объекте состояния можно видеть, что соответствующее ему значение с плавающей запятой — это 0.25; 0.25*4 = 1.0. Второе число — это «0101» (=5), а результирующее значение — 0.5*4 = 2.0. Третье число — это «0010», которое равняется 2, а соответствующее ему значение — 0.666*4 = 2.666, которое достаточно близко к 3, но не равно этому числу. Понятно, что при применении 4-битных значений мы столкнёмся с некоторой потерей точности. Последнее значение, «0011» — это 3, ему соответствует 1.000*4 = 4.0.&lt;/p&gt;
  &lt;p id=&quot;gHYe&quot;&gt;Понятно, что нет большой необходимости выполнять подобные вычисления вручную. С помощью &lt;code&gt;bitsandbytes&lt;/code&gt; можно выполнить и обратное преобразование:&lt;/p&gt;
  &lt;pre id=&quot;03xy&quot;&gt;x = bf.dequantize_fp4(x_4bit, qstate)
print(x)

# &amp;gt; tensor([1.000, 2.000, 2.666, 4.000])&lt;/pre&gt;
  &lt;p id=&quot;o8jp&quot;&gt;4-битный формат чисел тоже обладает ограниченным диапазоном значений. Например, массив &lt;code&gt;[1.0, 2.0, 3.0, 64.0]&lt;/code&gt; будет преобразован в &lt;code&gt;[0.333, 0.333, 0.333, 64.0]&lt;/code&gt;. Но для более или менее нормализованных данных он даёт совсем неплохие результаты. Давайте, для примера, нарисуем синусоиду, воспользовавшись данными в формате FP4:&lt;/p&gt;
  &lt;pre id=&quot;CMEJ&quot;&gt;import matplotlib.pyplot as plt
import numpy as np
from bitsandbytes import functional as bf

length = np.pi * 4
resolution = 256
xvals = np.arange(0, length, length / resolution)
wave = np.sin(xvals)

x_4bit, qstate = bf.quantize_fp4(torch.tensor(wave, dtype=torch.float32, device=device), blocksize=64)
dq = bf.dequantize_fp4(x_4bit, qstate)

plt.rcParams[&amp;quot;figure.figsize&amp;quot;] = (14, 5)
plt.title(&amp;#x27;FP8 Sine Wave&amp;#x27;)
plt.plot(xvals, wave)
plt.plot(xvals, dq.cpu().numpy())
plt.show()&lt;/pre&gt;
  &lt;p id=&quot;xD7m&quot;&gt;Тут, что неудивительно, видны некоторые потери точности, но то, что получилось, выглядит довольно прилично.&lt;/p&gt;
  &lt;figure id=&quot;Llyp&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/8a6/1a5/42c/8a61a542c3ed08a4a7ed1e9d1ccf8617.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Синусоидальная волна, построенная по данным, представленным в формате FP4 (изображение подготовлено автором)&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;jwHw&quot;&gt;Если же говорить о типе NF4 — читатели сами могут попробовать исследовать его с помощью методов &lt;code&gt;quantize_nf4&lt;/code&gt; и &lt;code&gt;dequantize_nf4&lt;/code&gt;; весь код останется таким же, как прежде. Но, к сожалению, на момент написания этой статьи 4-битные типы данных работают лишь с CUDA; вычисления на CPU пока не поддерживаются.&lt;/p&gt;
  &lt;h2 id=&quot;E2b2&quot;&gt;Тестирование&lt;/h2&gt;
  &lt;p id=&quot;55C0&quot;&gt;Теперь, в роли финального этапа этой статьи, предлагаю создать нейросетевую модель и протестировать её. При использовании Python‑библиотеки &lt;a href=&quot;https://huggingface.co/docs/transformers/main_classes/quantization&quot; target=&quot;_blank&quot;&gt;transformers&lt;/a&gt; можно загрузить заранее обученную модель в 4-битном формате. Для этого достаточно установить в &lt;code&gt;True&lt;/code&gt; параметр &lt;code&gt;load_in_4-bit&lt;/code&gt;. Но будем честны: это не приблизит нас к пониманию того, как новые форматы чисел влияют на нейросетевые модели. Вместо этого прибегнем к «игрушечному» примеру — создадим маленькую нейросеть, обучим её и воспользуемся ей, применив 4-битные числа.&lt;/p&gt;
  &lt;p id=&quot;ThlR&quot;&gt;Для начала создадим нейросетевую модель:&lt;/p&gt;
  &lt;pre id=&quot;grKW&quot;&gt;import torch
import torch.nn as nn
import torch.optim as optim
from typing import Any

class NetNormal(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.model = nn.Sequential(
            nn.Linear(784, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 10)
        )
      
    def forward(self, x):
        x = self.flatten(x)
        x = self.model(x)
        return F.log_softmax(x, dim=1)&lt;/pre&gt;
  &lt;p id=&quot;U2yr&quot;&gt;Теперь надо подготовить загрузчик набора данных. Я буду использовать набор данных &lt;a href=&quot;https://pytorch.org/vision/0.15/generated/torchvision.datasets.MNIST.html&quot; target=&quot;_blank&quot;&gt;MNIST&lt;/a&gt;, содержащий 70000 изображений рукописных цифр размером 28x28 (авторские права на этот набор данных принадлежат Яну Лекуну и Коринне Кортез, он доступен по лицензии &lt;a href=&quot;https://creativecommons.org/licenses/by-sa/3.0/&quot; target=&quot;_blank&quot;&gt;Creative Commons Attribution-Share Alike 3.0&lt;/a&gt;). Набор данных разделён на две части — 60000 учебных и 10000 тестовых изображений. Выбор загружаемых данных может быть выполнен в загрузчике путём использования параметра &lt;code&gt;train=True|False&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;U1Nr&quot;&gt;from torchvision import datasets, transforms

train_loader = torch.utils.data.DataLoader(
    datasets.MNIST(&amp;quot;data&amp;quot;, train=True, download=True,
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=batch_size, shuffle=True)

test_loader = torch.utils.data.DataLoader(
    datasets.MNIST(&amp;quot;data&amp;quot;, train=False, transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=batch_size, shuffle=True)&lt;/pre&gt;
  &lt;p id=&quot;aSzj&quot;&gt;Теперь мы готовы к тому, чтобы обучить и сохранить модель. Процесс обучения выполняется «нормальным» способом, с применением стандартного формата чисел.&lt;/p&gt;
  &lt;pre id=&quot;WIgw&quot;&gt;device = torch.device(&amp;quot;cuda&amp;quot;)

batch_size = 64
epochs = 4
log_interval = 500

def train(model: nn.Module, train_loader: torch.utils.data.DataLoader,
          optimizer: Any, epoch: int):
    &amp;quot;&amp;quot;&amp;quot; Train the model &amp;quot;&amp;quot;&amp;quot;
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        
        if batch_idx % log_interval == 0:
            print(f&amp;#x27;Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)}]\tLoss: {loss.item():.5f}&amp;#x27;)
            
def test(model: nn.Module, test_loader: torch.utils.data.DataLoader):
    &amp;quot;&amp;quot;&amp;quot; Test the model &amp;quot;&amp;quot;&amp;quot;
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            t_start = time.monotonic()
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction=&amp;#x27;sum&amp;#x27;).item()
            pred = output.argmax(dim=1, keepdim=True)
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    t_diff = time.monotonic() - t_start

    print(f&amp;quot;Test set: Average loss: {test_loss:.4f}, Accuracy: {correct}/{len(test_loader.dataset)} ({100. * correct / len(test_loader.dataset)}%)\n&amp;quot;)

def get_size_kb(model: nn.Module):
    &amp;quot;&amp;quot;&amp;quot; Get model size in kilobytes &amp;quot;&amp;quot;&amp;quot;
    size_model = 0
    for param in model.parameters():
        if param.data.is_floating_point():
            size_model += param.numel() * torch.finfo(param.data.dtype).bits
        else:
            size_model += param.numel() * torch.iinfo(param.data.dtype).bits
    print(f&amp;quot;Model size: {size_model / (8*1024)} KB&amp;quot;)

# Обучение
model = NetNormal().to(device)
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
for epoch in range(1, epochs + 1):
    train(model, train_loader, optimizer, epoch)
    test(model, test_loader)

get_size(model)

# Сохранение
torch.save(model.state_dict(), &amp;quot;mnist_model.pt&amp;quot;)&lt;/pre&gt;
  &lt;p id=&quot;zewV&quot;&gt;Я, кроме того, написал вспомогательный метод &lt;code&gt;get_size_kb&lt;/code&gt;, позволяющий узнать размер модели в килобайтах.&lt;/p&gt;
  &lt;p id=&quot;aDlX&quot;&gt;Вот как выглядит процесс обучения модели:&lt;/p&gt;
  &lt;pre id=&quot;nsDA&quot;&gt;Train Epoch: 1 [0/60000] Loss: 2.31558
Train Epoch: 1 [32000/60000] Loss: 0.53704
Test set: Average loss: 0.2684, Accuracy: 9225/10000 (92.25%)

Train Epoch: 2 [0/60000] Loss: 0.19791
Train Epoch: 2 [32000/60000] Loss: 0.17268
Test set: Average loss: 0.1998, Accuracy: 9401/10000 (94.01%)

Train Epoch: 3 [0/60000] Loss: 0.30570
Train Epoch: 3 [32000/60000] Loss: 0.33042
Test set: Average loss: 0.1614, Accuracy: 9530/10000 (95.3%)

Train Epoch: 4 [0/60000] Loss: 0.20046
Train Epoch: 4 [32000/60000] Loss: 0.19178
Test set: Average loss: 0.1376, Accuracy: 9601/10000 (96.01%)

Model size: 427.2890625 KB&lt;/pre&gt;
  &lt;p id=&quot;K91u&quot;&gt;Наша простая модель достигла точности в 96%, размер нейронной сети — 427 Кб.&lt;/p&gt;
  &lt;p id=&quot;HvCY&quot;&gt;А теперь — самое интересное! Создадим и протестируем 8-битную версию модели. Описание модели будет, на самом деле, таким же, как прежде. Я лишь заменил слой &lt;code&gt;Linear&lt;/code&gt; на слой &lt;code&gt;Linear8bitLt&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;QpHD&quot;&gt;from bitsandbytes.nn import Linear8bitLt

class Net8Bit(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.model = nn.Sequential(
            Linear8bitLt(784, 128, has_fp16_weights=False),
            nn.ReLU(),
            Linear8bitLt(128, 64, has_fp16_weights=False),
            nn.ReLU(),
            Linear8bitLt(64, 10, has_fp16_weights=False)
        )
      
    def forward(self, x):
        x = self.flatten(x)
        x = self.model(x)
        return F.log_softmax(x, dim=1)

device = torch.device(&amp;quot;cuda&amp;quot;)

# Загрузка
model = Net8Bit()
model.load_state_dict(torch.load(&amp;quot;mnist_model.pt&amp;quot;))
get_size_kb(model)
print(model.model[0].weight)

# Преобразование
model = model.to(device)

get_size_kb(model)
print(model.model[0].weight)

# Запуск
test(model, test_loader)&lt;/pre&gt;
  &lt;p id=&quot;PUXB&quot;&gt;Вот — выходные данные:&lt;/p&gt;
  &lt;pre id=&quot;vFd2&quot;&gt;Model size: 427.2890625 KB
Parameter(Int8Params([[ 0.0071,  0.0059,  0.0146,  ...,  0.0111, -0.0041,  0.0025],
            ...,
            [-0.0131, -0.0093, -0.0016,  ..., -0.0156,  0.0042,  0.0296]]))

Model size: 107.4140625 KB
Parameter(Int8Params([[  9,   7,  19,  ...,  14,  -5,   3],
            ...,
            [-21, -15,  -3,  ..., -25,   7,  47]], device=&amp;#x27;cuda:0&amp;#x27;,
           dtype=torch.int8))

Test set: Average loss: 0.1347, Accuracy: 9600/10000 (96.0%)&lt;/pre&gt;
  &lt;p id=&quot;sVj6&quot;&gt;Исходная модель была загружена с использованием стандартного формата чисел с плавающей запятой. Её размер остался таким же, веса выглядят как &lt;code&gt;[0.0071, 0.0059,…]&lt;/code&gt;. Вся «магия» заключается в преобразовании модели в &lt;code&gt;cuda&lt;/code&gt; — она становится в 4 раза меньше. Как видно, значения весов находятся в одном и том же диапазоне, поэтому преобразование модели сложностей не вызывает. В процессе проверки модели на тестовых данных оказалось, что она не потеряла ни единого процента точности!&lt;/p&gt;
  &lt;p id=&quot;YU8E&quot;&gt;А теперь — 4-битная версия:&lt;/p&gt;
  &lt;pre id=&quot;qO3R&quot;&gt;from bitsandbytes.nn import LinearFP4, LinearNF4

class Net4Bit(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.model = nn.Sequential(
            LinearFP4(784, 128),
            nn.ReLU(),
            LinearFP4(128, 64),
            nn.ReLU(),
            LinearFP4(64, 10)
        )
      
    def forward(self, x):
        x = self.flatten(x)
        x = self.model(x)
        return F.log_softmax(x, dim=1)

# Загрузка
model = Net4Bit()
model.load_state_dict(torch.load(&amp;quot;mnist_model.pt&amp;quot;))
get_model_size(model)
print(model.model[2].weight)

# Преобразование
model = model.to(device)

get_model_size(model)
print(model.model[2].weight)

# Запуск
test(model, test_loader)&lt;/pre&gt;
  &lt;p id=&quot;73JA&quot;&gt;Вот — результаты работы:&lt;/p&gt;
  &lt;pre id=&quot;GheQ&quot;&gt;Model size: 427.2890625 KB
Parameter(Params4bit([[ 0.0916, -0.0453,  0.0891,  ...,  0.0430, -0.1094, -0.0751],
            ...,
            [-0.0079, -0.1021, -0.0094,  ..., -0.0124,  0.0889,  0.0048]]))

Model size: 54.1015625 KB
Parameter(Params4bit([[ 95], [ 81], [109],
            ...,
            [ 34], [ 46], [ 33]], device=&amp;#x27;cuda:0&amp;#x27;, dtype=torch.uint8))

Test set: Average loss: 0.1414, Accuracy: 9579/10000 (95.79%)&lt;/pre&gt;
  &lt;p id=&quot;szlh&quot;&gt;Мы получили интересные результаты. После преобразования размер модели уменьшился в 8 раз — с 427 до 54 Кб, но точность упала лишь на 1%. Как это возможно? Ответить на этот вопрос несложно. По крайней мере — для этой модели:&lt;/p&gt;
  &lt;ul id=&quot;wX7p&quot;&gt;
    &lt;li id=&quot;QAf9&quot;&gt;Как видно, веса распределены более или менее равномерно, и потеря точности не слишком велика.&lt;/li&gt;
    &lt;li id=&quot;Bw0x&quot;&gt;При обработке выходных данных в модели используется &lt;a href=&quot;https://pytorch.org/docs/stable/generated/torch.nn.functional.log_softmax.html&quot; target=&quot;_blank&quot;&gt;Softmax&lt;/a&gt;, результат определяется по индексу максимального значения. Несложно понять, что при поиске максимального индекса само значение роли не играет. Например — между 0,8 и 0,9 нет никакой разницы в том случае, если другие значения — это 0,1 или 0,2.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;LJwq&quot;&gt;Полагаю — важно более тщательно изучить то, что у нас получилось. Загрузим числа из тестового набора данных и ознакомимся с тем, что выдаст модель.&lt;/p&gt;
  &lt;pre id=&quot;Wag3&quot;&gt;dataset = datasets.MNIST(&amp;#x27;data&amp;#x27;, train=False, transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ]))

np.set_printoptions(precision=3, suppress=True)  # Не использовать научную запись

data_in = dataset[4][0]
for x in range(28):
    for y in range(28):
        print(f&amp;quot;{data_in[0][x][y]: .1f}&amp;quot;, end=&amp;quot; &amp;quot;)
    print()&lt;/pre&gt;
  &lt;p id=&quot;DDvf&quot;&gt;Вот — выведенное на экран число, которое нужно распознать:&lt;/p&gt;
  &lt;figure id=&quot;pxWy&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/193/1b8/325/1931b83257f4954f94d3e46c403e5bb9.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Вывод данных&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;4LGr&quot;&gt;Посмотрим, что выдаст «стандартная» модель:&lt;/p&gt;
  &lt;pre id=&quot;IU1T&quot;&gt;# Подавить научную запись
np.set_printoptions(precision=2, suppress=True)  

# Прогноз
with torch.no_grad():
    output = model(data_in.to(device))
    print(output[0].cpu().numpy())
    ind = output.argmax(dim=1, keepdim=True)[0].cpu().item()
    print(&amp;quot;Result:&amp;quot;, ind)

# &amp;gt; [ -8.27 -13.89  -6.89 -11.13  -0.03  -8.09  -7.46  -7.6   -6.43  -3.77]
# &amp;gt; Result: 4&lt;/pre&gt;
  &lt;p id=&quot;4ezF&quot;&gt;Максимальный элемент находится в 5-й позиции (элементы в массивах numpy нумеруются с 0), что соответствует числу 4.&lt;/p&gt;
  &lt;p id=&quot;esAJ&quot;&gt;Вот — результаты работы 8-битной модели:&lt;/p&gt;
  &lt;pre id=&quot;Nnfi&quot;&gt;# &amp;gt; [ -9.09 -12.66  -8.42 -12.2   -0.01  -9.25  -8.29  -7.26  -8.36  -4.45]
# &amp;gt; Result: 4&lt;/pre&gt;
  &lt;p id=&quot;aY5z&quot;&gt;Вот что выдала 4-битная модель:&lt;/p&gt;
  &lt;pre id=&quot;3wgV&quot;&gt;# &amp;gt; [ -8.56 -12.12  -7.52 -12.1   -0.01  -8.94  -7.84  -7.41  -7.31  -4.45]
# &amp;gt; Result: 4&lt;/pre&gt;
  &lt;p id=&quot;5kTk&quot;&gt;Хорошо видно, что реальные выходные значения у разных моделей различаются, но индекс максимального элемента остаётся одним и тем же.&lt;/p&gt;
  &lt;h2 id=&quot;5PWl&quot;&gt;Итоги&lt;/h2&gt;
  &lt;p id=&quot;QsM6&quot;&gt;В этой статье мы исследовали разные способы представления 16-битных, 8-битных и 4-битных чисел с плавающей запятой. Мы создали нейронную сеть и смогли запустить её с применением 8-битных и 4-битных чисел. И, на самом деле, за тем, как она работает, было интересно наблюдать. Уменьшая точность используемых чисел — со стандартной до 4-битной, нам удалось снизить объём памяти, необходимый модели, в 8 раз, при этом потеря точности оказалась минимальной. Конечно, мы экспериментировали на «игрушечном» примере, в по‑настоящему больших моделях используются более сложные механизмы (тем, кто интересуется данной темой, рекомендую &lt;a href=&quot;https://huggingface.co/blog/hf-bitsandbytes-integration&quot; target=&quot;_blank&quot;&gt;этот&lt;/a&gt; материал).&lt;/p&gt;
  &lt;p id=&quot;mEaO&quot;&gt;Надеюсь, эта статья помогла вам получить представление об общих идеях, лежащих в основе вычислений с плавающей запятой. Как известно, «нужда — мать изобретений». Уменьшение объёма памяти, занимаемой моделью, в 4–8 раз — это замечательное достижение, особенно учитывая разницу в цене между видеокартами с памятью в 8, 16, 32 и 64 Гб ;).&lt;/p&gt;
  &lt;p id=&quot;pUcR&quot;&gt;Кстати, даже 4 бита — это уже не предел. В публикации о &lt;a href=&quot;https://arxiv.org/pdf/2210.17323.pdf&quot; target=&quot;_blank&quot;&gt;GTPQ&lt;/a&gt; была упомянута возможность квантификации весов в 2 или даже в три (1,5 бита!) состояния. И последнее — по порядку, но не по важности: интересно поразмышлять о «точности» нейротрансмиттеров человеческого мозга. Интуитивно понятно, что она не так уж и высока. Возможно, 2- или 4-битные нейросетевые модели ближе, чем другие, к тем «моделям», которые находятся в наших головах.&lt;/p&gt;
  &lt;p id=&quot;i7i5&quot;&gt;О, а приходите к нам работать? 🤗 💰&lt;/p&gt;
  &lt;p id=&quot;39bw&quot;&gt;Теги:&lt;/p&gt;
  &lt;ul id=&quot;v15V&quot;&gt;
    &lt;li id=&quot;vmV7&quot;&gt;&lt;a href=&quot;https://habr.com/ru/search/?target_type=posts&amp;order=relevance&amp;q=%5BPython%5D&quot; target=&quot;_blank&quot;&gt;Python&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;pSi0&quot;&gt;&lt;a href=&quot;https://habr.com/ru/search/?target_type=posts&amp;order=relevance&amp;q=%5B%D1%80%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0%5D&quot; target=&quot;_blank&quot;&gt;разработка&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;siTh&quot;&gt;&lt;a href=&quot;https://habr.com/ru/search/?target_type=posts&amp;order=relevance&amp;q=%5B%D1%87%D0%B8%D1%81%D0%BB%D0%B0%20%D1%81%20%D0%BF%D0%BB%D0%B0%D0%B2%D0%B0%D1%8E%D1%89%D0%B5%D0%B9%20%D0%B7%D0%B0%D0%BF%D1%8F%D1%82%D0%BE%D0%B9%5D&quot; target=&quot;_blank&quot;&gt;числа с плавающей запятой&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;nq5l&quot;&gt;Хабы:&lt;/p&gt;
  &lt;ul id=&quot;8YR7&quot;&gt;
    &lt;li id=&quot;0Jwh&quot;&gt;&lt;a href=&quot;https://habr.com/ru/companies/wunderfund/articles/&quot; target=&quot;_blank&quot;&gt;Блог компании Wunder Fund&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;uQCs&quot;&gt;&lt;a href=&quot;https://habr.com/ru/hubs/webdev/&quot; target=&quot;_blank&quot;&gt;Веб-разработка&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;Kwyg&quot;&gt;&lt;a href=&quot;https://habr.com/ru/hubs/python/&quot; target=&quot;_blank&quot;&gt;Python&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;vVCP&quot;&gt;&lt;a href=&quot;https://habr.com/ru/hubs/programming/&quot; target=&quot;_blank&quot;&gt;Программирование&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;

</content></entry></feed>