Tech Workshop ... Genetic Algorithms: The original “vibe-coding”

Oh my, I’m going to set off all forms of radiation alarms with the nuclear carpet bombing this will bring upon me but I’m still going to say it in full: “I’m sorry, vibe-coders, but you’re retro-techies >:3”. Granted I’m simplifying a little bit but once you read what I’m talking about, you’ll see that I touched the “original vibe-coding”, which may even be older than me. So sit back and get ready to become grey super quick.

Back in my uni studies, I was attending a course called “Biology-inspired Computing”. The subject was pretty neat, focused on applying principles found in natural world to computing tasks such as chip design optimisation, runtime hardware adaptation (look up stuff like evolvable hardware) and even a concept called DNA-computing (literally using a specific artificial strand of DNA to perform computation — it’s really, really slow so not really viable for real-life application). In the chip design and optimisation part a lot of emphasis was on concepts of usage of genetic algorithms to design a configuration for an FPGA (Field-Programmable Gate Array, basically a chip which can be programmed to work as any piece of hardware — no, you can’t fit an entire x86 CPU in that) and even implementing such algorithms in the FPGA itself to have a chip which can reconfigure itself during its use.

Sounds pretty cool, doesn’t it? And it’s a surprisingly simple idea. Let me demonstrate with a piece of code

func_blocks = []

class GenCircuit():
    def __init__(self):
        self.__config_blocks = [ rand(0, len(func_blocks)) ]
        self.__viability = 0

    @property
    def config_string(self);
        return "".join(self.__config_blocks)
    
    @property
    def viability(self):
        return self.__viability

    def splice(self, instance_b: GenCircuit):
        splice_point = rand(0, len(self.__config_blocks))
        cut_a = self.__config_blocks[splice_point:]
        cut_b = instance_b.__config_blocks[splice_point:]
        
        self.__config_blocks = self.__config_blocks[0:splice_point] + cut_b
        instance_b.__config_blocks = instance_b.__config_blocks[0:splice_point] + cut_a

    def mutate(self):
        mutation_point = rand(0, len(self.__config_blocks))
        self.__config_blocks[mutation_point] = rand(0, len(func_blocks))
        
    def compute(self, *args):
        # simulate whatever the block is supposed to do here
        pass
    
    def evaluate(self, scoreboard):
        for test in scoreboard:
            result = self.compute(*test)
            if result == test.result:
                self.__viability += 1
    
if __name__ == "__main__":
    generations = 1000
    max_instances = 10
    splice_chance = 10
    mutation_chance = 0.1 # always significantly lower than splice chance
    
    scoreboard = []
    viability_threshold = len(scoreboard) * 0.9
    for _ in range(generations):
        instances = instances + [GenCircuit() for _ in range(max_instances - len(instances))]
        
        # evaluate the population
        for i in instances:
            i.evaluate(scoreboard)
        
        # discard non-viable instances
        instances = [ i for i in instances if i.viability >= viability_threshold]    
                
        # splice instances
        for i in instances:
            if rand(0,100) <= splice_chance:
                i.splice(instances(rand(0, len(instances))))
                
        # mutate instances
        for i in instances:
            if rand(0,100) <= mutation_chance:
                i.mutate()
                
    for i in instances:
        print(i.config_string)

The code above is a basic skeleton (it won't work so if you want to toy with it, adjust as necessary) of a genetic algorithm. It consists of a population of randomly generated instances of the GenCircuit which are then evaluated against the scoreboard depending on what your goal is. The most viable instances are then taken into the next generation but before that, they're randomly spliced with one another and some can be mutated before the next generation starts. At the end of the run, you'll end up with instances which are most viable at performing the task you defined in the scoreboard.

The biology-inspired parts are quite on the nose here. Splicing means you cross the “genetic code” of two instances and mutation means the instance gets randomly altered in certain spot. The core principle however is quite simple: generate random “results”, check how good they are, mix them together and maybe change some of them, rinse and repeat. Basically, you have a guided and very specifically constrained RNG.

“Wait, the core principle of that sounds a lot like vibe-coding. That too essentially does random stuff and watches what works!” YES, EXACTLY! Except vibe-coding has far weaker constraints. In what way? The scoreboard is the key. The scoreboard, which in the example is empty and would likely fit a fairly simple task (say, something like a mathematical unit), it's one of the key components because it precisely defines the characteristics of the result. You could say it's a definition of the testbench (technical term in HW verification but also works in the general sense). Putting that scoreboard together requires understanding of the task the final design is supposed to fulfill. And guess what vibe-coding lacks? Exactly.

Now, you might be thinking “But can't genetic algorithm come up with a better solution than the traditional approach?” I'll surprise you but there are instances in which it can in fact come up with a solution that's correct AND more efficient than a traditional design on the same hardware. BUT ... there's a significant caveat. That solution is going to work ONLY and ONLY on that one specific chip or set-up. If you try to transfer it somewhere else, it may not perform well or even not work at all. Why? That's still being investigated and studied but it's quite interesting that the algorithm can sometimes leverage even tiny deviations which happen during the manufacturing process.

But portability, or lack thereof, isn't the only problem. Optimisation is also a non-trivial task because, sure, the correct and reliable output (emphasis on reliable) is critical but it's what use is for a solution which you can't use in the end because you haven't got the resources to run it or it takes ages to produce the result? This is where genetic algorithms get complicated because optimising the solutions to multiple criteria is where the science of this field sits and yes, there are genetic algorithms which are indeed capable of that.

“Ok, the code above seems kind of trivial. It can't be that resource intensive,” you might say. Oh my sweet summer child, the code above will be capable of “resolving” something trivial too. But if you want something really complex, get ready to burn hours and hours of computation on supercomputers and vast majority of that will be verification. Worse, you might end up with a population of solutions that aren't viable at all because there's a small error in the algorithm itself and you have to do the entire process all over again. Need to change the parameters? Full rerun of the algorithm. Adding another parameter? Yup, you guess it, full re-run. See where I'm going with this? Getting these solutions is horrendously inefficient in terms of compute time.

“Wait, can't I just change the resulting code?” What code? In the example above there's no code at all. That example is coded in a way that would produce a finished configuration string to load into the chip (by the way, this is only possible with chips where you know the format of the string in terms of how the blocks and their connections are encoded). Fair enough that you'll likely end up with a generated code, be it Verilog or VHDL (I'm more familiar with the latter from my school years but my current job taught me the former “the hard way”) or if you're using this for software (at which point, who hurt you?) a generated pile of code in the language of your choice. And keep in mind that code has no idea about libraries and/or coding practices because it doesn't care about readability. So ... have fun trying to optimise, let alone debug this. My condolences to your sanity. More often than not the only way is to run the algorithm again (prompt the AI?), possibly tweak the criteria a bit (“engineer” the propmt?) and hope that you were right this time around.

So yeah, I’m sorry vibe-coders but me and quite a lot of people from my generation have done your “magic trick” before it was “free, cool and in” (good lord, I really am a fossil X3). Back then it was resource-heavy, hard to use for complex tasks and absolute nightmare to tweak. And we knew quite well what we wanted to achieve. Your “circus number” is even more resource heavy, also hard to use for complex tasks (if it’s usable at all) and also an absolute nightmare to work with. And as a cherry on top, you have no idea what you want.

So sorry, vibe-coders, you’ve been deprecated before you even became cool >:3

R.R.A.