Edifice Architecture Zoo
Setup
Choose one of the two cells below depending on how you started Livebook.
Standalone (default)
Use this if you started Livebook normally (livebook server).
Most models work on CPU. Uncomment the EXLA lines for GPU acceleration.
edifice_dep =
if File.dir?(Path.expand("~/edifice")) do
{:edifice, path: Path.expand("~/edifice")}
else
{:edifice, "~> 0.2.0"}
end
Mix.install([
edifice_dep,
# {:exla, "~> 0.10"},
{:kino, "~> 0.14"}
])
# Nx.global_default_backend(EXLA.Backend)
Attached to project (recommended for Nix/CUDA)
Use this if you started Livebook via ./scripts/livebook.sh.
This gives full access to EXLA with CUDA — no extra setup needed.
# Easiest — run the helper script from nix-shell:
./scripts/livebook.sh
# Or manually in two terminals (from nix-shell):
# Terminal 1: ./scripts/livebook.sh node
# Terminal 2: ./scripts/livebook.sh livebook
Skip the cell above and evaluate this one instead — all deps including EXLA with CUDA are already loaded.
Nx.global_default_backend(EXLA.Backend)
IO.puts("Attached mode — using EXLA backend from project node")
Introduction
Edifice ships with 111+ neural network architectures across 17 families — from state-space models to graph neural networks to diffusion models. This notebook builds one representative from each family, inspects its shape, and counts its parameters. No training — just a tour of the zoo.
Every model is built with the same API: Edifice.build(:name, opts).
families = Edifice.list_families()
family_counts =
families
|> Enum.sort_by(fn {_k, v} -> -length(v) end)
|> Enum.map(fn {family, archs} ->
%{"Family" => Atom.to_string(family), "Count" => length(archs)}
end)
total = Enum.sum(Enum.map(family_counts, & &1["Count"]))
IO.puts("#{total} architectures across #{length(family_counts)} families\n")
family_counts
|> Enum.each(fn %{"Family" => f, "Count" => c} ->
IO.puts(" #{String.pad_trailing(f, 18)} #{c}")
end)
Helper: Build, Init, and Inspect
This helper builds a model, initializes parameters with a template, runs one forward pass, and returns a summary map with shapes and parameter count.
defmodule Zoo do
def inspect_model(name, build_fn, input_fn) do
try do
model = build_fn.()
input = input_fn.()
template =
Map.new(input, fn {k, v} -> {k, Nx.template(Nx.shape(v), Nx.type(v))} end)
{init_fn, predict_fn} = Axon.build(model)
params = init_fn.(template, Axon.ModelState.empty())
output = predict_fn.(params, input)
param_count = count_params(params)
input_shapes =
Enum.map_join(input, ", ", fn {k, v} ->
"#{k}: #{inspect(Nx.shape(v))}"
end)
output_shape = get_shape(output)
%{
name: name,
status: :ok,
input: input_shapes,
output_shape: output_shape,
params: param_count
}
rescue
e ->
%{name: name, status: :fail, error: Exception.message(e) |> String.slice(0, 80)}
end
end
def count_params(%Axon.ModelState{} = state) do
state
|> Axon.ModelState.trainable_parameters()
|> count_nested(0)
end
# Handle container outputs (e.g. VAE encoder returns %{mu: tensor, log_var: tensor})
def get_shape(%Nx.Tensor{} = t), do: Nx.shape(t)
def get_shape(map) when is_map(map) do
map |> Enum.map(fn {k, v} -> "#{k}: #{inspect(get_shape(v))}" end) |> Enum.join(", ")
end
def get_shape(tuple) when is_tuple(tuple), do: tuple |> Tuple.to_list() |> Enum.map(&get_shape/1)
def get_shape(other), do: other
defp count_nested(%Nx.Tensor{} = t, acc), do: acc + Nx.size(t)
defp count_nested(map, acc) when is_map(map) do
Enum.reduce(map, acc, fn {_k, v}, a -> count_nested(v, a) end)
end
defp count_nested(_other, acc), do: acc
def fmt_params(n) when n >= 1_000_000, do: "#{Float.round(n / 1_000_000, 1)}M"
def fmt_params(n) when n >= 1_000, do: "#{Float.round(n / 1_000, 1)}K"
def fmt_params(n), do: "#{n}"
def print_results(results, pad \\ 15) do
for r <- results do
case r.status do
:ok ->
IO.puts(" #{String.pad_trailing(to_string(r.name), pad)} output=#{inspect(r.output_shape)} params=#{fmt_params(r.params)}")
:fail ->
IO.puts(" #{String.pad_trailing(to_string(r.name), pad)} FAIL: #{r.error}")
end
end
end
end
Shared Dimensions
We use small dimensions so everything runs fast on CPU.
# Shared small dims
batch = 2
embed = 32
hidden = 16
seq_len = 8
num_classes = 4
image_size = 16
in_channels = 3
num_nodes = 6
node_dim = 16
num_points = 12
point_dim = 3
latent_size = 8
rand = fn shape ->
{tensor, _key} = Nx.Random.normal(Nx.Random.key(42), shape: shape)
tensor
end
# Shared sequence model options (used by SSM, recurrent, attention families)
seq_opts = [
embed_dim: embed,
hidden_size: hidden,
state_size: 8,
num_layers: 2,
seq_len: seq_len,
window_size: seq_len,
head_dim: 8,
num_heads: 2,
dropout: 0.0
]
seq_input = fn -> %{"state_sequence" => rand.({batch, seq_len, embed})} end
flat_input = fn -> %{"input" => rand.({batch, embed})} end
:ok
SSM Family
State-space models process sequences through learned state transitions. Representatives: Mamba (selective SSM), S4 (structured SSM), GatedSSM (gated variant).
results = [
Zoo.inspect_model(:mamba, fn -> Edifice.build(:mamba, seq_opts) end, seq_input),
Zoo.inspect_model(:s4, fn -> Edifice.build(:s4, seq_opts) end, seq_input),
Zoo.inspect_model(:gated_ssm, fn -> Edifice.build(:gated_ssm, seq_opts) end, seq_input)
]
Zoo.print_results(results)
Recurrent Family
Classic and modern recurrent networks with sequential state updates.
results = [
Zoo.inspect_model(:lstm, fn -> Edifice.build(:lstm, seq_opts) end, seq_input),
Zoo.inspect_model(:gru, fn -> Edifice.build(:gru, seq_opts) end, seq_input),
Zoo.inspect_model(:xlstm, fn -> Edifice.build(:xlstm, seq_opts) end, seq_input),
Zoo.inspect_model(:min_gru, fn -> Edifice.build(:min_gru, seq_opts) end, seq_input)
]
Zoo.print_results(results)
Attention Family
Transformer-style attention and efficient variants.
results = [
Zoo.inspect_model(:gqa, fn -> Edifice.build(:gqa, seq_opts) end, seq_input),
Zoo.inspect_model(:retnet, fn -> Edifice.build(:retnet, seq_opts) end, seq_input),
Zoo.inspect_model(:fnet, fn -> Edifice.build(:fnet, seq_opts) end, seq_input),
Zoo.inspect_model(:performer, fn -> Edifice.build(:performer, seq_opts) end, seq_input)
]
Zoo.print_results(results)
Transformer Family
Full decoder-only transformer with KV-cache support.
result = Zoo.inspect_model(:decoder_only,
fn ->
Edifice.build(:decoder_only,
embed_dim: embed,
hidden_size: hidden,
num_heads: 2,
num_kv_heads: 1,
num_layers: 2,
seq_len: seq_len,
dropout: 0.0
)
end,
seq_input
)
Zoo.print_results([result])
Feedforward Family
Non-sequential models for tabular and structured data.
results = [
Zoo.inspect_model(:mlp,
fn -> Edifice.build(:mlp, input_size: embed, hidden_sizes: [hidden]) end,
fn -> %{"input" => rand.({batch, embed})} end
),
Zoo.inspect_model(:tabnet,
fn -> Edifice.build(:tabnet, input_size: embed, output_size: num_classes) end,
fn -> %{"input" => rand.({batch, embed})} end
),
Zoo.inspect_model(:kan,
fn -> Edifice.build(:kan, seq_opts) end,
seq_input
)
]
Zoo.print_results(results)
Vision Family
Image models from ViTs to U-Nets.
image_input = fn -> %{"image" => rand.({batch, in_channels, image_size, image_size})} end
results = [
Zoo.inspect_model(:vit,
fn ->
Edifice.build(:vit,
image_size: image_size, in_channels: in_channels,
patch_size: 4, embed_dim: hidden, depth: 1, num_heads: 2, dropout: 0.0
)
end,
image_input
),
Zoo.inspect_model(:convnext,
fn ->
Edifice.build(:convnext,
image_size: 32, in_channels: in_channels, patch_size: 4,
dims: [hidden, hidden * 2], depths: [1, 1], dropout: 0.0
)
end,
fn -> %{"image" => rand.({batch, in_channels, 32, 32})} end
),
Zoo.inspect_model(:unet,
fn ->
Edifice.build(:unet,
in_channels: in_channels, out_channels: 1, image_size: image_size,
base_features: 8, depth: 2, dropout: 0.0
)
end,
image_input
)
]
Zoo.print_results(results)
Convolutional Family
Traditional and modern convolutional architectures.
results = [
Zoo.inspect_model(:resnet,
fn ->
Edifice.build(:resnet,
input_shape: {nil, image_size, image_size, in_channels},
num_classes: num_classes, block_sizes: [1, 1], initial_channels: 8
)
end,
fn -> %{"input" => rand.({batch, image_size, image_size, in_channels})} end
),
Zoo.inspect_model(:tcn,
fn -> Edifice.build(:tcn, input_size: embed, hidden_size: hidden, num_layers: 2) end,
fn -> %{"input" => rand.({batch, seq_len, embed})} end
)
]
Zoo.print_results(results)
Graph Family
Graph neural networks that operate on nodes + adjacency matrices.
graph_opts = [
input_dim: node_dim, hidden_dim: hidden, num_classes: num_classes,
num_layers: 2, num_heads: 2, dropout: 0.0
]
graph_input = fn ->
nodes = rand.({batch, num_nodes, node_dim})
adj = Nx.eye(num_nodes) |> Nx.broadcast({batch, num_nodes, num_nodes})
%{"nodes" => nodes, "adjacency" => adj}
end
results = [
Zoo.inspect_model(:gcn, fn -> Edifice.build(:gcn, graph_opts) end, graph_input),
Zoo.inspect_model(:gat, fn -> Edifice.build(:gat, graph_opts) end, graph_input),
Zoo.inspect_model(:graph_transformer, fn -> Edifice.build(:graph_transformer, graph_opts) end, graph_input)
]
Zoo.print_results(results, 20)
Sets Family
Permutation-invariant models for point clouds and unordered sets.
results = [
Zoo.inspect_model(:deep_sets,
fn -> Edifice.build(:deep_sets, input_dim: point_dim, output_dim: num_classes) end,
fn -> %{"input" => rand.({batch, num_points, point_dim})} end
),
Zoo.inspect_model(:pointnet,
fn -> Edifice.build(:pointnet, num_classes: num_classes, input_dim: point_dim) end,
fn -> %{"input" => rand.({batch, num_points, point_dim})} end
)
]
Zoo.print_results(results)
Energy Family
Energy-based and dynamics models that learn scalar energy functions or ODEs.
results = [
Zoo.inspect_model(:ebm,
fn -> Edifice.build(:ebm, input_size: embed) end, flat_input),
Zoo.inspect_model(:hopfield,
fn -> Edifice.build(:hopfield, input_dim: embed) end, flat_input),
Zoo.inspect_model(:neural_ode,
fn -> Edifice.build(:neural_ode, input_size: embed, hidden_size: hidden) end, flat_input)
]
Zoo.print_results(results)
Probabilistic Family
Models that quantify prediction uncertainty.
results = [
Zoo.inspect_model(:bayesian,
fn -> Edifice.build(:bayesian, input_size: embed, output_size: num_classes) end, flat_input),
Zoo.inspect_model(:mc_dropout,
fn -> Edifice.build(:mc_dropout, input_size: embed, output_size: num_classes) end, flat_input),
Zoo.inspect_model(:evidential,
fn -> Edifice.build(:evidential, input_size: embed, num_classes: num_classes) end, flat_input)
]
Zoo.print_results(results)
Memory Family
Models with external memory for reasoning and one-shot learning.
num_memories = 4
memory_dim = 8
results = [
Zoo.inspect_model(:ntm,
fn ->
Edifice.build(:ntm,
input_size: embed, output_size: hidden,
memory_size: num_memories, memory_dim: memory_dim, num_heads: 1
)
end,
fn -> %{"input" => rand.({batch, embed}), "memory" => rand.({batch, num_memories, memory_dim})} end
),
Zoo.inspect_model(:memory_network,
fn ->
Edifice.build(:memory_network,
input_dim: embed, output_dim: hidden, num_memories: num_memories
)
end,
fn -> %{"query" => rand.({batch, embed}), "memories" => rand.({batch, num_memories, embed})} end
)
]
Zoo.print_results(results, 18)
Meta Family
Mixture-of-experts, adapters, and other meta-learning components.
results = [
Zoo.inspect_model(:moe,
fn ->
Edifice.build(:moe,
input_size: embed, hidden_size: hidden * 4,
output_size: hidden, num_experts: 2, top_k: 1
)
end,
fn -> %{"moe_input" => rand.({batch, seq_len, embed})} end
),
Zoo.inspect_model(:lora,
fn -> Edifice.build(:lora, input_size: embed, output_size: hidden, rank: 4) end,
fn -> %{"input" => rand.({batch, embed})} end
),
Zoo.inspect_model(:capsule,
fn ->
Edifice.build(:capsule,
input_shape: {nil, 28, 28, 1}, conv_channels: 32, conv_kernel: 9,
num_primary_caps: 8, primary_cap_dim: 4,
num_digit_caps: num_classes, digit_cap_dim: 4
)
end,
fn -> %{"input" => rand.({batch, 28, 28, 1})} end
)
]
Zoo.print_results(results)
Generative Family
Models that generate new data: VAEs, GANs, diffusion, flow matching.
action_dim = 4
action_horizon = 4
results = [
Zoo.inspect_model(:vae_encoder,
fn ->
{enc, _dec} = Edifice.build(:vae, input_size: embed, latent_size: latent_size)
enc
end,
fn -> %{"input" => rand.({batch, embed})} end
),
Zoo.inspect_model(:gan_generator,
fn ->
{gen, _disc} = Edifice.build(:gan, output_size: embed, latent_size: latent_size)
gen
end,
fn -> %{"noise" => rand.({batch, latent_size})} end
),
Zoo.inspect_model(:diffusion,
fn ->
Edifice.build(:diffusion,
obs_size: embed, action_dim: action_dim, action_horizon: action_horizon,
hidden_size: hidden, num_layers: 2, dropout: 0.0
)
end,
fn ->
%{
"noisy_actions" => rand.({batch, action_horizon, action_dim}),
"timestep" => rand.({batch}),
"observations" => rand.({batch, embed})
}
end
),
Zoo.inspect_model(:normalizing_flow,
fn -> Edifice.build(:normalizing_flow, input_size: embed, num_flows: 2) end,
fn -> %{"input" => rand.({batch, embed})} end
)
]
Zoo.print_results(results, 20)
Contrastive / Self-Supervised Family
Representation learning without labels.
results = [
Zoo.inspect_model(:simclr,
fn -> Edifice.build(:simclr, encoder_dim: embed, projection_dim: hidden) end,
fn -> %{"features" => rand.({batch, embed})} end
),
Zoo.inspect_model(:byol_online,
fn ->
{online, _target} = Edifice.build(:byol, encoder_dim: embed, projection_dim: hidden)
online
end,
fn -> %{"features" => rand.({batch, embed})} end
),
Zoo.inspect_model(:vicreg,
fn -> Edifice.build(:vicreg, encoder_dim: embed, projection_dim: hidden) end,
fn -> %{"features" => rand.({batch, embed})} end
)
]
Zoo.print_results(results)
Liquid Family
Liquid neural networks with continuous-time dynamics.
result = Zoo.inspect_model(:liquid,
fn -> Edifice.build(:liquid, seq_opts) end,
seq_input
)
Zoo.print_results([result])
Neuromorphic Family
Spiking neural networks inspired by biological neurons.
results = [
Zoo.inspect_model(:snn,
fn ->
Edifice.build(:snn, input_size: embed, output_size: num_classes, hidden_sizes: [hidden])
end,
fn -> %{"input" => rand.({batch, embed})} end
),
Zoo.inspect_model(:ann2snn,
fn -> Edifice.build(:ann2snn, input_size: embed, output_size: num_classes) end,
fn -> %{"input" => rand.({batch, embed})} end
)
]
Zoo.print_results(results)
Summary
All 17 families in one table:
families = Edifice.list_families()
summary =
for {family, archs} <- Enum.sort_by(families, fn {_k, v} -> -length(v) end) do
names = Enum.map_join(archs, ", ", &Atom.to_string/1)
names =
if String.length(names) > 60 do
String.slice(names, 0, 57) <> "..."
else
names
end
IO.puts(
" #{String.pad_trailing(Atom.to_string(family), 16)} " <>
"#{String.pad_trailing(Integer.to_string(length(archs)), 4)} " <>
names
)
end
total = families |> Map.values() |> List.flatten() |> length()
IO.puts("\n Total: #{total} architectures")
IO.puts("\n All accessible via: Edifice.build(:name, opts)")