Specification

Design Space 01: Static vs Dynamic Shapes

Series: AI-Native Programming Language Research — Design Space Memos Document 1 of 6 (planned: shapes, AD-through-effects, sparsity, distribution, verification, hardware abstraction)


1. Problem statement

Every tensor operation has a shape — at minimum, rank and per-dimension size, but in practice also dtype, layout, device, and (increasingly) sharding. The fundamental design question is: when is the shape known, and who is responsible for proving it correct?

The two ends of the spectrum:

Neither pure end has won, and the interesting work in the last five years has been at the symbolic middle: shapes are values that may be unknown but are subject to compile-time constraints and arithmetic, and the compiler can specialize, guard, or fall back depending on what it learns.

This memo maps the spectrum, surveys the points along it, compares them, and identifies open problems.


2. What "shape" actually means

A useful distinction before going further. "Shape" in the loose sense is everything the compiler needs to know about a tensor to generate code:

Component Examples Variability
Rank 2, 3, 4 Almost always known statically
Dimension sizes [B, S, D] The interesting axis — often dynamic in B and S
Dtype f16, bf16, i8 Usually static; quantization complicates
Layout NCHW, NHWC, blocked, packed Often a compile choice, not user-facing
Device cuda:0, tpu:3, cpu Static at the placement layer, dynamic across hosts
Sharding Sharded(axis=0, mesh=M) Increasingly part of the type (GSPMD, JAX pjit)
Stride / alignment Contiguous, strided views Compiler-internal in most languages

The static-vs-dynamic question applies to each component independently. Most languages are static in dtype and rank but disagree on dimension sizes and sharding. When people say "static shapes" they almost always mean dimension sizes known at compile time.


3. The spectrum

Five distinguishable points, ordered from most dynamic to most static:

3.1 Fully dynamic (NumPy, eager PyTorch, eager TensorFlow)

Shapes are runtime metadata. Every op does a shape check; broadcasting rules are evaluated at call time. There is no static guarantee that A @ B is well-formed.

Strengths: zero annotation burden, trivially supports data-dependent shapes (variable-length sequences, ragged batches, dynamic masking).

Weaknesses: errors surface late and far from cause; compilation can't fuse without speculative tracing; memory planning is reactive (alloc on demand) rather than precomputed; no IDE assistance for shape errors.

3.2 Tracing-based (JAX jit, TensorFlow AutoGraph, PyTorch's torch.jit.trace)

The user writes dynamic-looking code. The system traces an execution with abstract values, recovers a graph specialized to the input shape, and compiles it. Subsequent calls with the same shape hit the cache; new shapes trigger retracing.

Strengths: keeps the dynamic feel; the compiler sees a fully shaped graph and can apply XLA-class optimizations.

Weaknesses: the recompilation cliff. Production LLM serving with variable sequence lengths or batch sizes hits this constantly. Tracing also can't see Python-level control flow that depends on runtime values; users write jax.lax.cond and jax.lax.scan to work around it. Side effects during tracing have notoriously subtle semantics.

3.3 Symbolic shapes (PyTorch 2 / TorchDynamo + Inductor, TVM Relax, MLIR shape dialect, ONNX dynamic axes)

Shapes are first-class symbolic values at compile time. A dimension might be s0 (unknown but consistent across uses), s0 + 1 (arithmetic), or s0 * s1 (products). The compiler reasons about constraints, generates guards, and emits one kernel that handles a family of shapes.

This is where most current research and engineering effort sits, and it's the most pragmatic answer for LLM workloads where sequence lengths and batch sizes are genuinely dynamic.

Strengths: one compilation handles a family of shapes; integrates with autotuning; supports data-dependent shapes via guards and graph breaks.

Weaknesses: symbolic shape inference is hard — equality of s0 * 2 and s0 + s0 requires arithmetic reasoning. Guards are easy to over-specialize, leading to recompilations. Implementations are heuristic-heavy; the design space (which guards to insert, when to specialize, when to fall back) is unsettled.

3.4 Statically annotated, non-dependent (Hasktorch type-level shapes, tch-rs typed tensors, NamedTensor proposals)

Shapes go in types, but as type-level naturals or constants — not as values that can be computed over. You write Tensor [Batch, 768] Float, and the type checker matches them syntactically.

Strengths: catches shape errors at compile time without dependent types; works in mainstream type systems (Haskell, Rust, Scala) with type-level naturals.

Weaknesses: shape polymorphism is painful. Writing a function that works for any sequence length means parameterizing over type-level naturals, and the resulting signatures get heavy. Arithmetic on type-level naturals (e.g., proving M+N = N+M) often requires hand-written equality witnesses.

3.5 Fully dependent (Dex, Futhark, ATS, Idris, F*)

Shapes are ordinary values that happen to appear in types. dot : (n : Nat) -> Vec n F -> Vec n F -> F is a real type. The compiler proves shape compatibility through ordinary type checking.

Strengths: the language has nothing special to say about tensors — they're just instances of a more general dependent type machinery. Shape arithmetic is just arithmetic. Highest level of static guarantee.

Weaknesses: dependent type systems are hard. Type inference is undecidable in general; user-facing error messages are notoriously bad. Existing dependent languages (Idris, Agda, Lean) have research-prototype-level tooling. Dex made this work for arrays specifically with index sets, but the surface syntax has a learning curve.


4. Comparison

Fully dynamic Tracing Symbolic Static (non-dep) Fully dependent
Examples NumPy, PyTorch eager JAX jit, TF AutoGraph PyTorch 2, Relax, MLIR Hasktorch, tch-rs Dex, Futhark, ATS
Shape errors caught at Run Trace Compile (with guards) Compile Compile
Annotation burden None None Optional High (per-fn) High (per-fn)
Shape polymorphism Trivial (untyped) Per-trace Symbolic, native Painful Native
Data-dependent shapes Trivial Limited (graph break) Limited (guard + fallback) Painful Painful (need erasure)
Compilation overhead None Per-shape recompile Family compile Per-call Per-call
Optimization quality Low (per-op dispatch) High Medium-high High High
Memory planning Reactive Precomputed Precomputed (with bounds) Precomputed Precomputed
IDE/tooling support Linter only Limited Improving Mainstream Research-grade
LLM workload fit Good Poor (recompile) Good Poor Poor
Scientific compute fit Mediocre Excellent Excellent Good Excellent

The diagonal here matters: tracing and symbolic shapes look superficially similar to dynamic shapes from the user's perspective, but the compiler infrastructure and consequences are completely different.


5. Tradeoff axes in depth

5.1 The recompilation cliff

This is the single biggest practical problem with pure static shape systems. JAX shipped jax.jit in 2018; production users hit recompilation on every new batch size or sequence length. The mitigations have evolved:

  1. Bucketing: pad inputs to a small set of shapes. Wastes compute, complicates batching.
  2. Polymorphic export (jax2tf, JAX shape polymorphism): export a graph with symbolic dimensions that lowers to an XLA program with dynamic shapes.
  3. Symbolic shapes from the start (PyTorch 2): never compile a fully static graph; always parameterize over symbolic dims.

The pattern: what started as a static-shape system is, under load, growing a symbolic-shape layer. This is empirical evidence that pure static shapes are too rigid for production ML, especially LLM serving.

5.2 Shape polymorphism vs ergonomics

The deepest design tension. The function matmul should work for any compatible shapes, which means its type must be polymorphic in shape. Naive dependent typing makes this:

matmul : (m n k : Nat) -> Tensor [m, k] -> Tensor [k, n] -> Tensor [m, n]

That's tolerable. But chain three matmuls, do a softmax, an attention computation, and the type signature dwarfs the body. Real research questions:

Dex's index-set approach handles this for pointful programs (where you write loops over indices) but not for pointfree programs (where you compose tensor functions). Mojo punts on this entirely so far.

5.3 Data-dependent shapes

A tensor whose shape depends on a runtime value. Examples:

Pure static-shape systems can't express these without falling back to fully dynamic. Symbolic shapes handle them via guards: "compile assuming s0 ≤ 1024, generate a fallback for the violation case." Dependent types handle them via existentials: Σ n. Tensor [n].

The hard cases are control flow depending on shape (e.g., MoE routing where each expert sees a different number of tokens). These are still bolted-on in essentially every system.

5.4 Sharding and shape

Distributed tensors blew up the shape question in the last three years. A [batch, seq, hidden] tensor sharded across a [data, model] mesh has different effective shapes per-device, and the compiler needs to reason about both the logical shape and the physical sharding.

GSPMD, JAX pjit/shard_map, and PyTorch DTensor all handle this with varying levels of compiler integration. The unsolved question: should sharding be in the type? GSPMD treats it as compile-time annotation that the compiler propagates. A more native answer would put Sharded(axis=0, mesh=M) in the tensor's type and let unification handle resharding. Nobody has done this cleanly.

5.5 Memory planning

Static shapes enable upfront memory planning: the compiler computes peak memory, picks an allocation strategy, and emits explicit alloc/free. Dynamic shapes force reactive allocation (caching allocator in PyTorch).

Symbolic shapes can plan if bounds are known: "allocate enough for the maximum sequence length." The cost is overallocation when actual shapes are smaller. Production systems (vLLM's PagedAttention, FlashAttention's tiling) work around this with custom memory managers — another thing that wants to be a language feature.

5.6 LLM-era workloads as a stress test

Modern transformer inference is the worst case for pure static shapes:

A language designed today should treat these as first-class rather than as workarounds. None currently do.


6. Open research problems

  1. Gradual shape typing. Mix static and dynamic shapes within a single program with a clean interop story. Inspired by gradual typing (Siek, Taha). Dimensions you annotate get checked statically; unannotated dimensions are dynamic. Can the compiler insert guards at the boundary? Can shape inference fill in the gaps?

  2. Symbolic shape inference quality. PyTorch 2's symbolic shape engine is heuristic-heavy and not principled. Could you frame it as constraint solving over Presburger arithmetic? Polyhedral analysis (used in HPC compilers) handles this for affine cases — does it generalize?

  3. Sharding-shape unification. Treat sharding as part of the shape, with resharding inferred by the compiler when shapes meet at an operation boundary. Looks like a dimension-aware unifier with cost model.

  4. Differentiating through dynamic shapes. Reverse-mode AD over a graph with data-dependent shapes is genuinely awkward. What's the right semantics for nonzero, masking, MoE routing under autodiff? Some of this is folklore in the AD literature (Pearlmutter & Siskind, "Reverse-mode AD in a functional framework"); some is genuinely open.

  5. Effect-aware shape semantics. When shape depends on a random sample (rejection sampling, top-k) or a side effect, what does "static" even mean? This connects to the AD-through-effects memo (next document).

  6. Ergonomic dependent shape types. The Dex/Futhark approach is too heavy for mainstream adoption. The Hasktorch approach is too rigid. Is there a sweet spot — bidirectional type checking, refinement-style inference, effect-track shape constraints — that's expressive and ergonomic?

  7. Shape-aware autotuning. When the shape is symbolic, kernel selection becomes a function of shape, not a fixed choice. This is partially solved by Triton-style autotuning but not in a principled way.


7. Recommendation for an AI-native language design

If I were starting from scratch and had to pick a position:

The thesis: pure static is too rigid for production, pure dynamic is too unconstrained for the compiler, and the symbolic middle is where the real design work lives. Any language that picks an extreme will end up reinventing the middle anyway, so design for it from day one.


8. References

Foundational papers

Shape-typed array languages

Symbolic shapes and modern compilers

Sharding and distributed tensors

Dependent types and verification

Related work in array programming

Production / engineering writeups (less formal but important)


9. Next memo

Design Space 02: Differentiation through Effects. Reverse-mode AD over pure functions is well-understood; AD over programs with mutation, randomness, exceptions, or I/O is genuinely open. This connects directly to several of the open problems above (data-dependent shapes, MoE routing, probabilistic programming) and is where effect systems intersect with autodiff in non-trivial ways.