<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Zmiennoprzecinkowe — Inferownia</title><link>https://inferownia.pl/tags/zmiennoprzecinkowe/</link><description>Inferownia — polski hub wiedzy o lokalnym AI: uruchamianie modeli językowych na własnym sprzęcie, kwantyzacja, inferencja, małe modele, fine-tuning i RAG.</description><language>pl-PL</language><copyright>&#169; 2026 Inferownia</copyright><lastBuildDate>Sun, 19 Jul 2026 00:00:00 +0200</lastBuildDate><atom:link href="https://inferownia.pl/tags/zmiennoprzecinkowe/index.xml" rel="self" type="application/rss+xml"/><item><title>bf16, f16, f32 — o co chodzi z tymi bitami</title><link>https://inferownia.pl/poradniki/bf16-f16-f32/</link><pubDate>Sun, 19 Jul 2026 00:00:00 +0200</pubDate><category>kwantyzacja</category><guid>https://inferownia.pl/poradniki/bf16-f16-f32/</guid><description>Konwertujesz model do GGUF-a, widzisz --outtype f16 i --outtype bf16, i przez sekundę udajesz przed samym sobą, że wiesz, którego wybrać. Rozbieramy te formaty na bity — znak, wykładnik, mantysa — i tłumaczymy, czemu bf16 wygrał trening ML, choć jest mniej dokładny niż f16.</description><content:encoded><![CDATA[<p>Odpalasz <code>convert_hf_to_gguf.py</code> na świeżym modelu z Hugging Face, lecisz z palca <code>--outtype</code>
i nagle podpowiedź wypluwa ci listę: <code>f32</code>, <code>f16</code>, <code>bf16</code>, <code>q8_0</code>… Zatrzymujesz się na tych
trzech pierwszych. <code>f16</code> znasz, <code>f32</code> to „ten większy&quot;, ale <code>bf16</code> mruga jak nieznajomy na
imprezie, o którym wszyscy mówią, że jest spoko, tylko nikt nie potrafi powiedzieć czemu.
Klikasz <code>f16</code>, bo tak robiłeś zawsze, i ruszasz dalej — a z tyłu głowy zostaje to samo pytanie,
które wraca za każdym razem, gdy w konfiguracji treningu widzisz <code>compute_dtype=torch.bfloat16</code>:
czym one się właściwie różnią, skoro obie „szesnastki&quot; ważą tyle samo?</p>
<p>To jeden z klasycznych sporów o dwie „szesnastki&quot; — ktoś wrzuca skwantyzowany model, ktoś inny zaraz pyta,
czemu baza jest w <code>bf16</code>, a nie <code>f16</code>, i rozkręca się wątek na sto odpowiedzi, z których połowa
myli precyzję z zakresem. Bo to naprawdę nieoczywiste. Dwa formaty, oba po 16 bitów, oba po
2 bajty na liczbę — a jednak jeden wygrał trening wielkich modeli, a drugi został przy inferencji
i grafice. Cała różnica siedzi w tym, jak te 16 bitów pokroić. No to pokrójmy.</p>
<h2 id="liczba-zmiennoprzecinkowa-to-trzy-szufladki">Liczba zmiennoprzecinkowa to trzy szufladki</h2>
<p>Każda liczba w tych formatach — czy to <code>f32</code>, czy <code>f16</code>, czy <code>bf16</code> — składa się z trzech kawałków.
Wyobraź sobie zapis naukowy z podstawówki: −1,5 × 10⁴. Masz tam znak (minus), masz cyfry
znaczące (<code>1,5</code>) i masz potęgę, która mówi, gdzie postawić przecinek (<code>10⁴</code>). Komputer robi
dokładnie to samo, tylko na potęgach dwójki, i pakuje to w trzy pola bitów:</p>
<ul>
<li><strong>bit znaku</strong> (sign bit) — jeden bit, plus albo minus. Tu bez niespodzianek.</li>
<li><strong>wykładnik</strong> (exponent) — potęga dwójki, czyli <em>skala</em> liczby. To on decyduje, jak wielką
albo jak małą liczbę w ogóle da się zapisać. Nazywamy to <strong>zakresem dynamicznym</strong>.</li>
<li><strong>mantysa</strong> (significand, po staremu <em>mantissa</em>) — cyfry znaczące, czyli <em>dokładność</em>. To ona
decyduje, ile pozycji po przecinku odróżnisz od siebie. Nazywamy to <strong>precyzją</strong>.</li>
</ul>
<p>I teraz sedno, które trzeba wbić do głowy jak mantrę: <strong>zakres bierze się z wykładnika, precyzja
z mantysy. To dwie niezależne osie.</strong> Możesz mieć liczbę o gigantycznym zakresie i marnej
dokładności — albo odwrotnie. Nie idą w parze. Cała gra w tych formatach polega na tym, ile
z szesnastu (albo trzydziestu dwóch) dostępnych bitów oddasz jednemu, a ile drugiemu.</p>
<p>Jak to rozdzielono w praktyce? Tak — i to jest tabelka, którą warto zapamiętać:</p>
<table>
	<thead>
			<tr>
					<th>Format</th>
					<th style="text-align: center">Znak</th>
					<th style="text-align: center">Wykładnik</th>
					<th style="text-align: center">Mantysa</th>
					<th style="text-align: center">Zakres (maks.)</th>
					<th style="text-align: center">Precyzja dziesiętna</th>
					<th style="text-align: center">Bajty</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>f32</strong> (single)</td>
					<td style="text-align: center">1</td>
					<td style="text-align: center">8</td>
					<td style="text-align: center">23</td>
					<td style="text-align: center">3,4 × 10³⁸</td>
					<td style="text-align: center">≈ 7 cyfr</td>
					<td style="text-align: center">4</td>
			</tr>
			<tr>
					<td><strong>f16</strong> (half)</td>
					<td style="text-align: center">1</td>
					<td style="text-align: center">5</td>
					<td style="text-align: center">10</td>
					<td style="text-align: center">65 504</td>
					<td style="text-align: center">≈ 3–4 cyfry</td>
					<td style="text-align: center">2</td>
			</tr>
			<tr>
					<td><strong>bf16</strong> (bfloat16)</td>
					<td style="text-align: center">1</td>
					<td style="text-align: center">8</td>
					<td style="text-align: center">7</td>
					<td style="text-align: center">3,4 × 10³⁸</td>
					<td style="text-align: center">≈ 2–3 cyfry</td>
					<td style="text-align: center">2</td>
			</tr>
	</tbody>
</table>
<p><code>f32</code>, czyli <em>single precision</em> z normy IEEE 754, to 1 bit znaku, 8 bitów wykładnika i 23 bity
mantysy.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> To złoty standard „normalnego&quot; liczenia na komputerze — szeroki zakres i siedem
cyfr znaczących. Kosztuje 4 bajty na liczbę i przez lata nikt się nad tym nie zastanawiał.</p>
<h2 id="dwie-szesnastki-dwa-różne-cięcia">Dwie szesnastki, dwa różne cięcia</h2>
<p>I teraz robi się ciekawie, bo <code>f16</code> i <code>bf16</code> to obie „połówki&quot; — po 16 bitów, po 2 bajty. Tyle że
tort pokrojony zupełnie inaczej.</p>
<p><code>f16</code>, czyli <em>half precision</em>, też z IEEE 754, tnie tak: 1 bit znaku, <strong>5 bitów wykładnika</strong>,
<strong>10 bitów mantysy</strong>.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> Zauważasz? Oddano dużo miejsca mantysie, mało wykładnikowi. Efekt:
przyzwoita precyzja jak na 16 bitów, ale <strong>wąziutki zakres</strong> — największa skończona liczba, jaką
<code>f16</code> w ogóle potrafi zapisać, to 65 504. Przekroczysz to i masz overflow, liczba ucieka
w nieskończoność. Zejdziesz za nisko — underflow, liczba znika do zera. Dla grafiki, skąd <code>f16</code>
się wywodzi, to bez znaczenia; kolory i tak siedzą w rozsądnym przedziale.</p>
<p><code>bf16</code> — <em>brain floating point</em>, dziecko Google Brain, i uwaga: <strong>nie jest częścią IEEE 754</strong> —
kroi zupełnie inaczej: 1 bit znaku, <strong>8 bitów wykładnika</strong>, <strong>7 bitów mantysy</strong>.<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> Ten
sam wykładnik co <code>f32</code>. Osiem bitów, ten sam bias 127, ta sama arytmetyka skali. A to znaczy, że
<strong><code>bf16</code> ma dokładnie taki sam zakres dynamiczny jak <code>f32</code></strong> — od jakichś 10⁻³⁸ do 3,4 × 10³⁸.
Google w dokumentacji TPU pisze to wprost, bez owijania: zakresy dynamiczne <code>bfloat16</code> i <code>float32</code>
są równoważne, <code>bfloat16</code> po prostu zjada połowę pamięci.<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup></p>
<p>Cena? Mantysa skurczona do siedmiu bitów. Czyli <code>bf16</code> jest <strong>mniej dokładny liczbowo niż <code>f16</code></strong> —
odróżnia mniej cyfr znaczących. I tu wielu ludzi się wykłada, bo intuicja podpowiada: skoro <code>bf16</code>
ma mniej precyzji, to jest gorszą szesnastką. A to nieprawda. To po prostu inny kompromis, zrobiony
pod inny cel.</p>
<h2 id="czemu-bf16-wygrał-trening-choć-jest-mniej-dokładny">Czemu bf16 wygrał trening, choć jest mniej dokładny</h2>
<p>To jest ta część, która zaskakuje każdego, kto pierwszy raz na to trafia. Bierzemy format o <em>gorszej</em>
precyzji i to on stał się domyślnym wyborem do trenowania największych modeli świata. Jak to możliwe?</p>
<p>Bo w treningu sieci neuronowej najgroźniejszym wrogiem nie jest to, że liczba jest odrobinę
niedokładna. Najgroźniejsze jest to, że liczba <strong>wypadnie poza zakres</strong>. Gradienty potrafią być
malutkie jak 10⁻⁷, a aktywacje potrafią chwilowo strzelić wysoko. <code>f16</code> ze swoim sufitem na
65 504 i płytką podłogą po prostu tego nie łapie — wartości albo overflowują do nieskończoności,
albo underflowują do zera, a wyzerowany gradient to gradient, który nic już nie uczy. Dlatego
trening w <code>f16</code> wymagał sztuczek w rodzaju <em>loss scalingu</em> — ręcznego podbijania skali strat, żeby
zmieścić gradienty w tym wąskim oknie.</p>
<p><code>bf16</code> rozwiązuje to od strzału, bo ma zakres <code>f32</code>. Gradient rzędu 10⁻³⁰? Mieści się. Aktywacja,
która na moment wystrzeliła? Też się mieści. Owszem, każda z tych liczb jest zapisana ciut mniej
dokładnie — ale sieci neuronowe są na taki szum zdumiewająco odporne, a na wypadnięcie z zakresu
już nie. Pokazało to twardo osobne badanie tego formatu — praca <em>„A Study of BFLOAT16 for Deep
Learning Training&quot;</em> z 2019 roku (zespół inżynierów Intela i Facebooka): trening w <code>bf16</code> osiąga te same
wyniki co <code>f32</code>, w tej samej liczbie iteracji, bez ruszania hiperparametrów, bez loss scalingu —
czego <code>f16</code> nigdy nie dawał za darmo.<sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup></p>
<div class="callout">
  <span class="callout__label">Dlaczego to ważne dla lokalnego LLM</span>
<p>Zapamiętaj to jako jedno zdanie: <strong><code>bf16</code> = zakres <code>f32</code>, precyzja gorsza niż <code>f16</code>; <code>f16</code> = precyzja
lepsza niż <code>bf16</code>, ale zakres wąski jak balkon.</strong> Do treningu i tam, gdzie liczy się stabilność
numeryczna, wybierasz zakres — więc <code>bf16</code> (na sprzęcie, który go wspiera natywnie; szybka akceleracja
<code>bf16</code> w Tensor Core to dopiero Ampere i nowsze). Dodatkowa dokładność <code>f16</code> na nic ci się zda, jeśli
połowa liczb ucieknie do nieskończoności. To dlatego „więcej bitów mantysy&quot; nie znaczy „lepszy format&quot;.</p>
</div>
<p>Jest jeszcze jeden bohater drugiego planu, o którym warto wiedzieć, żeby się nie pogubić: <strong>TF32</strong>
od NVIDIA. To nie jest format przechowywania danych — to wewnętrzny tryb obliczeniowy Tensor Core,
z 8-bitowym wykładnikiem (zakres jak <code>f32</code>) i 10-bitową mantysą (precyzja jak <code>f16</code>).<sup id="fnref1:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> Dane
w pamięci nadal leżą jako <code>f32</code>; karta tylko liczy na nich szybciej, po cichu przycinając mantysę
w trakcie mnożenia. Nie pomyl go z <code>bf16</code> jako typem tensora — to inna bajka, tryb liczenia, a nie
etykieta na danych.</p>
<h2 id="po-co-ci-to-przy-lokalnym-llm">Po co ci to przy lokalnym LLM</h2>
<p>„Fajnie, ale ja nic nie trenuję, tylko odpalam gotowca&quot; — i słusznie, więc przełóżmy to na twoje
podwórko.</p>
<p>Przy inferencji <code>f16</code> albo <code>bf16</code> to typowy <strong>compute dtype</strong>, czyli format, w którym faktycznie
lecą obliczenia — 2 bajty na parametr, dwa razy taniej niż <code>f32</code>, przy jakości wyjścia praktycznie
nie do odróżnienia. Ten sam podział bitów wraca też w <strong>KV-cache</strong>: bufor, w którym model trzyma
przemielony kontekst, standardowo siedzi w <code>f16</code> lub <code>bf16</code>, po 2 bajty na wartość — i przy długim
oknie to właśnie on, a nie wagi, robi się największym żłobem na VRAM (rozgryzaliśmy to bliżej
<a href="/poradniki/kv-cache-vram/">tutaj</a>). Compute dtype pojawi ci się też, jak zajrzysz do
<a href="/naukowy/qlora/">QLoRA</a> — tam wagi leżą w 4 bitach, ale liczenie i tak idzie w <code>bf16</code>.</p>
<p>A <code>f32</code>? Przy samej inferencji lokalnego LLM to zwykle <strong>przepłacanie pamięcią</strong>. Ładujesz dwa razy
cięższy tensor, zżerasz dwa razy więcej VRAM-u, a jakości odpowiedzi i tak nie odróżnisz od <code>f16</code>.
Pełne <code>f32</code> ma sens tam, gdzie liczy się stabilność akumulacji — sumowanie tysięcy iloczynów
cząstkowych, niektóre warstwy normalizacyjne, stany optymalizatora przy treningu. Nie przypadkiem
TPU mnoży w <code>bf16</code>, ale <strong>sumy akumuluje w <code>f32</code></strong>: to klasyka <em>mixed precision</em> — licz tanio, sumuj
dokładnie.<sup id="fnref1:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> Ale to dzieje się pod maską frameworka, nie w twoim <code>--outtype</code>.</p>
<p>No i konkret, od którego zaczęliśmy — konwersja do GGUF. Gdy robisz bazę pod dalszą kwantyzację,
<code>--outtype f16</code> albo <code>bf16</code> to punkt wyjścia: pełna szesnastka jako fundament, z którego dopiero
schodzisz do <code>Q4_K_M</code>, <code>Q5_K_M</code> czy <code>Q8_0</code> (o tych nazwach jest osobno
<a href="/poradniki/nazwy-kwantyzacji-gguf/">tu</a>). Prosta zasada kciuka:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># model wytrenowano w bf16 (dziś większość) -&gt; zachowaj zakres, bierz bf16:</span>
</span></span><span class="line"><span class="cl">python convert_hf_to_gguf.py ./Meta-Llama-3-8B --outtype bf16 --outfile llama3-8b-bf16.gguf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># starszy model natywnie w f16 albo docelowy sprzęt bez akceleracji bf16 -&gt; f16:</span>
</span></span><span class="line"><span class="cl">python convert_hf_to_gguf.py ./stary-model --outtype f16 --outfile model-f16.gguf
</span></span></code></pre></div><p>Skąd wiesz, w czym model żył oryginalnie? Zajrzyj do <code>config.json</code> na karcie modelu — pole
<code>torch_dtype</code>. Jeśli stoi tam <code>bfloat16</code> (a przy nowych modelach zwykle stoi), to konwersja do
<code>f16</code> po drodze przycięłaby zakres i mogłaby narobić drobnych przekłamań w skrajnych wartościach.
Trzymaj bazę w tym, w czym model się urodził. Nazwy <code>torch.float32</code>, <code>torch.float16</code> i <code>torch.bfloat16</code>
to zresztą dokładnie te trzy formaty z tabelki wyżej, tyle że w słowniku PyTorcha.<sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup></p>
<p>Bo cała ta szarpanina o bity sprowadza się do jednej sceny: masz 16 kratek do wypełnienia i decydujesz,
ile z nich pójdzie na „jak wielka może być liczba&quot;, a ile na „jak dokładnie ją znam&quot;. <code>f16</code> upchnął
dokładność i został z ciasnym balkonem zamiast horyzontu. <code>bf16</code> odpuścił parę cyfr po przecinku
i dostał w zamian cały zakres <code>f32</code> w połowie miejsca — i właśnie dlatego, kiedy następnym razem
zawahasz się nad <code>--outtype</code>, nie będziesz już zgadywał. Będziesz wiedział, że wybierasz nie „lepszą
szesnastkę&quot;, tylko odpowiedni kształt tego samego pudełka.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Struktura pola <code>binary32</code> (single precision) wg IEEE 754 — 1 bit znaku, 8 bitów wykładnika (bias 127), 23 bity mantysy, zakres do ok. 3,4 × 10³⁸, ok. 7 cyfr znaczących: <a href="https://en.wikipedia.org/wiki/Single-precision_floating-point_format">Single-precision floating-point format (Wikipedia / IEEE 754)</a>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Struktura pola <code>binary16</code> (half precision) wg IEEE 754-2008 — 1 bit znaku, 5 bitów wykładnika (bias 15), 10 bitów mantysy, maksymalna skończona wartość 65 504: <a href="https://en.wikipedia.org/wiki/Half-precision_floating-point_format">Half-precision floating-point format (Wikipedia / IEEE 754)</a>.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Dokładny układ bitów FP32 / FP16 / BF16 / TF32 (m.in. „FP32… 1 sign bit, an 8-bit exponent, and a 23-bit mantissa&quot;, FP16 max = 65504, TF32 jako tryb obliczeniowy Tensor Core): <a href="https://docs.nvidia.com/deeplearning/tensorrt/latest/inference-library/accuracy-considerations.html">NVIDIA TensorRT — Accuracy Considerations</a>.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>„The dynamic range of bfloat16 and float32 are equivalent. However, bfloat16 uses half of the memory space&quot; oraz mnożenie w bfloat16 z akumulacją w float32: <a href="https://docs.cloud.google.com/tpu/docs/bfloat16">Google Cloud — The bfloat16 numerical format</a>.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Empiryczny dowód, że trening w BF16 osiąga wyniki SOTA jak FP32, w tej samej liczbie iteracji i bez zmiany hiperparametrów, dzięki zachowaniu zakresu dynamicznego FP32: Kalamkar i in., „A Study of BFLOAT16 for Deep Learning Training&quot;, <a href="https://arxiv.org/abs/1905.12322">arXiv:1905.12322</a>.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Oficjalne nazwy i opisy typów <code>torch.float32</code>, <code>torch.float16</code> (half) oraz <code>torch.bfloat16</code> (Brain floating point): <a href="https://docs.pytorch.org/docs/stable/tensor_attributes.html">PyTorch — Tensor Attributes / torch.dtype</a>.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item></channel></rss>