Deep Learning from Scratch in 1400 Lines - Neve & Frost Framework A developer has released Frost, a deep learning framework written in roughly 1,400 lines of code, built on top of the Neve programming language. The framework includes parallel dataloaders and GPU kernels, and ships with a ResNet-18 benchmark that users can run themselves. The developer says the project targets three pain points in existing tooling: optimizer complexity in PyTorch, difficult C++/CUDA interoperability, and limited parallelism. The title literally means: "I have parallel dataloaders, GPU kernels and a high-performance computing programming language all expressed in a framework with 1400 lines of code". You may run the ResNet-18 benchmark yourself github.com/NoSavedDATA/Neve benchmarks https://github.com/NoSavedDATA/Neve benchmarks I released the Neve programming language a while ago. Now, this is the release of the Frost deep learning framework, alongside with the first benchmark. Other results for Neve: Currently working in a better GPU programming interface, towards the implementation of flash-attention. Neve documentation neve-lang.dev https://neve-lang.dev Neve repo github.com/NoSavedDATA/Neve https://github.com/NoSavedDATA/Neve . Youtube for updates youtube.com/@nosaveddata3994 https://www.youtube.com/@nosaveddata3994 . Discord for extensive talks/suggestions discord.gg/hP5feM7cV https://discord.gg/hP5feM7cV ──────────────────────────────────────── Once day I was reading some papers, and a very interesting paper was published. It was the sophia optimizer. I took a glance in an unnoficial code https://github.com/kyegomez/Sophia/blob/main/Sophia/main.py for it, and I questioned myself why did it have to be so difficult to add new optimizers in PyTorch. I experimented the optimizer, and the results were quite bad with a lot of NaNs. Turns out another paper published later claimed this and other optimizers had overstated claims. Imagine wasting hours studying a 10 pages of a paper, then hardly trying to debug it and asses whether other person discoveries are true. All that code reading complexity makes this a challengeful task. Problem 1: even optmizers are hard to understand in PyTorch. Few weeks later, flash attention https://arxiv.org/pdf/2205.14135 was released, and the algorithm actually achieved a speed-up of 2x. The problem, it was C++ CUDA. Most high-level GPU kernel frameworks were imature to the point the flash attention author chose not to use them. Now take a look what is necessary for adding C++ code in PyTorch python from setuptools import setup, Extension from torch.utils import cpp extension setup name="extension cpp", ext modules= cpp extension.CppExtension "extension cpp", "muladd.cpp" , extra compile args={ "cxx": define Py LIMITED API with min version 3.9 to expose only the stable limited API subset from Python.h "-DPy LIMITED API=0x03090000", define TORCH TARGET VERSION with min version 2.10 to expose only the stable API subset from torch "-DTORCH TARGET VERSION=0x020a000000000000", }, py limited api=True , Build 1 wheel across multiple Python versions cmdclass={'build ext': cpp extension.BuildExtension}, options={"bdist wheel": {"py limited api": "cp39"}} 3.9 is minimum supported Python version That comprehends problem 2: lack of high-level CUDA code and hard interoperability. For my Bachelor's thesis, I implemented the BBF https://github.com/NoSavedDATA/PyTorch-BBF-Bigger-Better-Faster-Atari-100k Reinforcement Learning for Atary. A bit before that, I took a glance code of the Efficient Zero https://github.com/YeWR/EfficientZero/tree/main/core reinforcement learning model. It has a parallelism that PyTorch does not handle, and the implementation required using Cython packages for having threads literaly coding in C, then just calling C functions from Python . Later, I realized PyTorch also needed to implement its data worker threads in C, another workaround over Python Global Interpreter Lock GIL . Not only that, even preprocessing implementations like the BPE are made in C, C++, Rust, etc... That leads us problem 3, lack of parallelism. That is when I decided to create a programming language, a few months before finishing my Bachelor's, which matured to my Master's project Summing up, currently, people must choose between languages like Python for high-level productivity, C and relatives for compute efficiency, Lua/Julia for advanced interoperability and other languages for concurrency. Thus, since in my job I had to wait hours for my neural networks to train, I decided to create a programming language in the time in between trainings. One language that had all these features, which are of high value for deep learning research. Nowadays, I believe it matured to such a point that it may be extended to other complex problem domains. Julia makes dynamic typying speed reach close to C++ speeds. It also has a mark sweep and channels for parallelism. The idea is very interesting. Let's take a look a in its cuda kernels. function mma kernel Z::CuDeviceMatrix{Float32}, X::CuDeviceMatrix{BFloat16}, Y::CuDeviceMatrix{BFloat16} Grid and block indices bx = blockIdx .x by = blockIdx .y Thread and warp indices tid = threadIdx .x lane = tid - 1 % 32 warp = tid - 1 ÷ 32 STOP Why am I seeing blockIdx .x in my code? Was this supposed to be a high-level scientific language or CUDA in C++28? Besides, it does not expose intrisics like the cp async, which is crucial for high-speed matrix multiplication. They must be explicitly added throgh interop intrisics. And it has the "end" keyword, which in my opinion incurs a lot of code pollution. Mojo has a Python interop, so it did not have to build all libs and frameworks from scratch +1 point. It has Byte-Pair Encoding benchmarks https://github.com/atsentia/mojo-tokenizer +1. It is only the BPE inference, no traning -1 point. It has flash-attention gpu kernels https://www.spheron.network/blog/modular-max-mojo-gpu-cloud-llm-inference/ +1 point. It runs MAX, which allows GPU code portability across different hardware, +2 points. It lacks channels, so I would hardly try to make a parallel dataloader in it. -2 points. It uses Rust ownership +0 points. Now let's look at Mojo kernels for the flash attention. @always inline def fused attention cpu BN: Int, BD: Int Q: LayoutTensor, K: LayoutTensor, V: LayoutTensor, O: LayoutTensor mut=True, ... , : comptime N = K.shape 0 comptime D = K.shape 1 comptime for tile n in range N // BN : var Q tile = Q.tile BN, D tile n, 0 comptime for tile d in range D // BD : var m 1 = LayoutTensor Q tile.dtype, Layout BN, 1 , MutAnyOrigin .stack allocation .fill Scalar Q tile.dtype .MIN var l 1 = LayoutTensor Q tile.dtype, Layout BN, 1 , MutAnyOrigin .stack allocation .fill 0 var O i = LayoutTensor Q tile.dtype, Layout.row major BN, BD , MutAnyOrigin .stack allocation .fill 0 comptime for tile n idx in range N // BN : var K tile = K.tile BN, D tile n idx, 0 var V tile = V.tile BN, BD tile n idx, tile d var S = matmul b transpose Q tile, K tile var m 2 = max m 1, rebind type of m 1 max axis=1 S Quite interesting. It has layouts and tiling, inspired by CuTe and Cutlass. It actually inspired the way Neve layouts and tiles work. Nevertheless, it still has a heavy syntax. Note the keyword comptime appears frequently a sort of metaprogramming . This adds some cognitive overhead. And the layouts are quite verbose. The layout fill and stack allocation can be simplified. Triton has layouts/tiling similar to Mojo, but is dynamically typed and has no comptime headaches. The problem is that Triton does not make Python Dataloaders easier to implement from the systems programming language perspective. We actually need a complete new programming language for this. python @triton.jit def attn fwd inner ... K block ptr = tl.advance K block ptr, 0, lo V block ptr = tl.advance V block ptr, lo, 0 loop over k, v and update accumulator for start kv in range lo, hi, BLOCK SIZE KV : Just let the compiler know that start n is a multiple of BLOCK N, so the compiler can do optimizations start kv = tl.multiple of start kv, BLOCK SIZE KV -- compute qk ---- K block = tl.load K block ptr QK block = tl.dot Q block, K block if STAGE == 2: mask = offs q :, None = start kv + offs kv None, : ... A LAYOUT Q block ptr = tl.make block ptr base=Q + qvk offset, shape= SEQ LEN, HEAD DIM , strides= stride Q seq, stride Q dim , offsets= block index q BLOCK SIZE Q, 0 , block shape= BLOCK SIZE Q, HEAD DIM , order= 1, 0 , ... Algebra -- compute qk ---- K block = tl.load K block ptr QK block = tl.dot Q block, K block Let's see how Neve GPU matrix multiplication looks like. gpu void @ layout