<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ai-spinach lab</title>
    <description>The latest articles on DEV Community by ai-spinach lab (@ai-spinachlab).</description>
    <link>https://dev.to/ai-spinachlab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4166926%2Fb20d3267-506c-4e3a-a37e-bae56318293a.png</url>
      <title>DEV Community: ai-spinach lab</title>
      <link>https://dev.to/ai-spinachlab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ai-spinachlab"/>
    <language>en</language>
    <item>
      <title>How a Browser Trains a Neural Network: Tensors as Textures, Ops as Shaders</title>
      <dc:creator>ai-spinach lab</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:13:44 +0000</pubDate>
      <link>https://dev.to/ai-spinachlab/how-a-browser-trains-a-neural-network-tensors-as-textures-ops-as-shaders-3lje</link>
      <guid>https://dev.to/ai-spinachlab/how-a-browser-trains-a-neural-network-tensors-as-textures-ops-as-shaders-3lje</guid>
      <description>&lt;p&gt;You can open a tab, click a button, and watch an LSTM's loss curve go down. No server, no Python environment, no CUDA. How does that work?&lt;/p&gt;

&lt;p&gt;The short answer: the browser borrows your graphics card through WebGL, and a few clever mappings turn a graphics API into a math engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a GPU at all?
&lt;/h2&gt;

&lt;p&gt;Training a neural network is mostly matrix multiplication: the same simple operation repeated over thousands of numbers. A CPU does a few things at a time very fast. A GPU does thousands of simple things at the same time. That is a good match.&lt;/p&gt;

&lt;p&gt;The catch is that WebGL was designed for drawing triangles, not for multiplying matrices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick: tensors become textures, ops become shaders
&lt;/h2&gt;

&lt;p&gt;Libraries like TensorFlow.js use two mappings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Machine learning&lt;/th&gt;
&lt;th&gt;WebGL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tensor (an array of numbers)&lt;/td&gt;
&lt;td&gt;Texture (an image stored in GPU memory)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operation (matmul, add, ReLU)&lt;/td&gt;
&lt;td&gt;Fragment shader (a small program that runs once per pixel)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Think of a texture as a grid of pixels, where each pixel's color channels hold numbers instead of colors. To "compute" something, the library draws a rectangle that covers the output texture. The GPU runs the fragment shader once for every output pixel, in parallel, and each run computes one element of the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JavaScript -&amp;gt; WebGL API -&amp;gt; GPU driver -&amp;gt; graphics hardware
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is being drawn on screen. The "image" is the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one training step looks like
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Forward pass.&lt;/strong&gt; Input tensors are uploaded as textures. Each layer is a chain of shader programs that read one texture and write another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss.&lt;/strong&gt; A final shader reduces the prediction error to a number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backward pass.&lt;/strong&gt; Gradients are just more operations, so they also run as shaders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update.&lt;/strong&gt; The optimizer (SGD, Adam) updates the weight textures, again on the GPU.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important detail: data stays on the GPU between steps. Only small things, like the loss value you want to print, come back to JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;tf&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tensorflow/tfjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webgl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sequential&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputShape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dense&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;train&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meanSquaredError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;epochs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;onEpochEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;epoch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;epoch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;xs&lt;/code&gt; and &lt;code&gt;ys&lt;/code&gt; are tensors of shape &lt;code&gt;[samples, 5, 1]&lt;/code&gt; and &lt;code&gt;[samples, 1]&lt;/code&gt;. Everything between &lt;code&gt;fit&lt;/code&gt; and the callback runs through the mappings above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it hurts
&lt;/h2&gt;

&lt;p&gt;WebGL gets the job done, but it has real limits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reading data back is slow.&lt;/strong&gt; Calling something like &lt;code&gt;dataSync()&lt;/code&gt; forces the CPU to wait for the GPU. Do it once per epoch, not once per step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First run is slower.&lt;/strong&gt; Shaders have to be compiled the first time each operation is used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory is not garbage collected.&lt;/strong&gt; Textures live on the GPU. Wrap temporary work in &lt;code&gt;tf.tidy()&lt;/code&gt; and call &lt;code&gt;dispose()&lt;/code&gt; on tensors you no longer need, or memory will creep up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Texture size and precision limits.&lt;/strong&gt; Large tensors are split across textures, and some devices, especially phones, may use lower precision than you expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No general-purpose compute.&lt;/strong&gt; WebGL has no compute shaders, so every operation is squeezed into a rendering pass. Newer APIs such as WebGPU are designed to fix this, but browser support still varies, so check before relying on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background tabs get throttled.&lt;/strong&gt; Browsers slow down timers and pause animation frames in hidden tabs, so a training loop can stall when you switch away. Keeping the tab in the foreground is the simple fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Practically, this means small to medium models: LSTMs with a few hundred units, small Transformers, time-series experiments. Not billion-parameter models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother training in the browser?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; The data never leaves the user's device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero server cost.&lt;/strong&gt; The user's GPU does the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant demos.&lt;/strong&gt; A link is all anyone needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is scale. For learning, prototyping, and visualizing how training behaves, it is hard to beat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;I built a small tool around this idea: you can train LSTM and Transformer sequence models on time-series data entirely in your browser, with live loss curves, at &lt;a href="https://www.ai-spinach.xyz" rel="noopener noreferrer"&gt;ai-spinach.xyz&lt;/a&gt;. The write-up above is part of its technical docs.&lt;/p&gt;

&lt;p&gt;If you have hit different limits with WebGL training, or you are experimenting with WebGPU, I would like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>javascript</category>
      <category>tensorflow</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
