Understand Neural Nets better, post 5 of N – Code Assistant shootout
In a series of previous blogposts [1, 2, 3, 4] I ran some experiments drawing the boundaries of the polytopes generated by a fully-connected leaky ReLU network while it was getting trained on reproducing an input image.
As I tried to scale the experiments to larger networks, I noticed a dramatic slowdown in the code, caused by the calculation of a hash of the activation pattern happening on CPU – so each training step would be fast, but then everything would grind to a halt for the visualisation, and for each pixel the code would forward-evaluate the NN (all in all 1024*1024 times), and whenever the prediction was calculated, it’d transfer the activation pattern to CPU and then perform the hashing. This was very slow, and very non-parallel.
I had contemplated writing some custom CUDA code to speed things up - there’s no reason to store the activation pattern or transfer it, the “right” way to solve the problem is computing a hash on the fly, ideally a hash with a commutative update function so the order in which the different ReLU neurons update the hash doesn’t matter.
Then again, this is a hobby project, and I don’t have the time to do anything overly smart for the moment. So I decided to - before doing anything sophisticated - I’ll see if I can have one of the two existing coding assistant that I use regularly solve the problem for me.
So I created two different directories, checked out the same base repo into both, created branches in both, and then queried both Gemini CLI and Claude Code perform the task, using the following prompt:
I then allowed both models to churn for a while. Both models provided changes, but Gemini failed to actually verify that the results are the same. Claude one-shotted the problem; Gemini needed the following additional prompt:
With that extra prodding / prompting, the solution provided by the model worked flawlessly, and was even a tiny bit faster than the Claude version.
Let’s look at the code that both models generated: The Gemini branch and the Claude branch. Reading the changes, a few things become clear:
1. Gemini shot itself in the foot on the RNG by generating a bunch of random hash coefficients, and that messed up the state of the RNG, so the training runs were no longer comparable pre/post change.
2. Gemini is using torch.matmul for the hash computation, whereas Claude is computing the hash as torch.sum( A * B ).
3. Claude has broken up the code in more smaller functions, whereas Gemini didn’t. Claude’s code is mildly more readable, Gemini’s is the more minimal change.
Interesting stuff. Neither solution is quite what I had in mind, but they are good enough for the moment, and provide a pretty significant speedup over the (also vibe-coded) stuff that I started out with. This is the first time for me that a coding assistant helped me optimize code in a nontrivial manner, and that’s … certainly something.
Anyhow, with these optimizations I can now run my data visualisation movie generation on slightly larger NNs with millions of parameters, so more studying ahead. I now need to figure out how to upload YouTube videos programmatically, but in the meantime, here is a video of training a 100-neuron, 10 layer deep network on the “circle drawing” task from my previous posts. Vibe coding randomly changed the color of my lines, but hey, that’s ok.
As per usual, there are more questions than answers in this video. The thing that puzzles me most is the relative “instability” of the training in later epochs. This is visible in “flickers” where seemingly randomly the SGD step hits on a vastly higher loss, with parts of the screen turning black and loss spiking, and then the training needs to recover. Interestingly, the geometry of the polytopes doesn’t change a lot in these situations, but the linear function on many of them changes at once, in a way that is very detrimental to overall performance. Once programmatic uploading works, I’ll upload many more videos, because one of the intriguing observations I have is the following:
When training diverges (for larger and deeper nets), the divergence starts by first messing up the linear functions, and only after they are gloriously messed up, the geometry of the polytopes starts to go haywire, too.
Until then!