← all posts

Hungarian history as an ultra-graph

Having trained a 1-bit language model on Anonymus, I had the Gesta Hungarorum on the brain — the origin story, Álmos and Árpád and the seven chieftains coming out of Scythia. And the library I built it on, ultragraph, is not really an ML library; it is a graph library that happens to be able to be a language model. So the obvious next move: stop training on Hungarian history and just draw it — the whole arc, as one graph, in the byte-graph data model itself.

Magyar történelem — the whole arc of Hungarian history as one ultra-graph

history is a graph, so store it as one

The ultragraph data model has exactly the three levels history wants:

So each era of Hungarian history is a sparse Tree. Its nodes are the people, events, and places that matter — Álmos, Mohács 1526, Vereckei-hágó — carried with their labels and years in the tree's ad-hoc side store (the byte-graph keeps one byte per node for the machine; the ad-hoc store keeps the human-readable extra data). Within an era, micro-edges wire the relations: father of, defeats, leads to, crowned. Between eras, ultra-edges wire the chronology — Origins >> Conquest >> Raids >> Founding >> … — one arrow per turn of the age.

ug = UltraGraph("hungarian_history")
trees = []
for key, span, nodes, edges in ERAS:
    t = Tree(len(nodes), name=key)
    t.adhoc["labels"] = nodes          # the ad-hoc side store: people / events / places
    for a, b, rel in edges:
        t.add_edge(a, b, 1)            # a micro-edge — one byte — inside the era
        t.adhoc["rels"][(a, b)] = rel
    ug.add(t); trees.append(t)
for prev, cur in zip(trees, trees[1:]):
    prev >> cur                        # ultra-edge (===) — the arrow of time

The result is one UltraGraph: 13 era-trees, 59 nodes, 45 micro-edges, 15 ultra-edges. The whole of Hungarian history, from before 895 to the EU, as a single byte-graph you can hold in one object.

the residual links

Chronology is the backbone, but history rhymes, so a few ultra-edges are typed residual instead of plain — the dashed purple brackets on the left of the diagram. They carry a signal across the centuries rather than step to step:

Residual edges are the same primitive the transformer blocks use to carry a signal past a layer. Here they carry it past a century. Same ===, longer reach.

the same substrate as the LLM

This is the part I find quietly satisfying. The Origins era at the top of the graph — Scythia, Levédia, Etelköz, Ügek, Álmos, the hetumoger — is exactly the material of the Gesta Hungarorum, the text the 1-bit Anonymus LLM was trained on. The graph and the language model are two readings of the same byte-graph substrate: one stores history as nodes and typed edges you can see; the other compresses a chronicle of it into ternary weights you can run. Node, edge, tree, ultra-edge — it renders a timeline or it writes medieval Latin, depending on which way you squint.

The graph here is semi-static — hand-curated, not learned. That is the honest label: it is a knowledge graph, a scaffold, the kind of thing you would hand to a model as ground truth rather than ask it to hallucinate. The interesting future is wiring the two together — the learned model reading and extending the curated graph — but that is a later post.

reproduce

pip install ultragraph-1bit
uv run python examples/hungarian_history.py
# -> examples/data/hungarian_history.svg (this timeline)
#    examples/data/hungarian_history_macro.svg (the library's own ultra-graph view)

The renderer is ~120 lines of pure Python emitting SVG — no dependencies beyond the library. Fork it, add your own eras, or point it at a different nation's arc; the data model does not care whose history it is.

Further reading

c00cb1d6bbc6834b8d5f318d27ea4362