Inferensys

Glossary

Graph Lowering

Graph lowering is the compiler process of transforming a high-level, abstract computational graph representation into a lower-level, hardware-specific representation through a series of legalization and conversion passes.
Stylish WeWork-like workspace with hot desks and document wall, professional searching through enterprise knowledge base on a mounted ultrawide display, warm industrial pendants overhead.
GRAPH COMPILATION STRATEGIES

What is Graph Lowering?

Graph lowering is a foundational compiler technique in machine learning that systematically transforms a high-level computational graph into a hardware-executable form.

Graph lowering is the process of transforming a high-level, abstract computational graph representation into a lower-level, hardware-specific representation through a series of legalization and conversion passes. This compiler-driven transformation is essential for executing neural networks on specialized accelerators like NPUs and GPUs, as it bridges the semantic gap between framework operators and native hardware instructions. The process involves decomposing complex operations into primitive kernels that the target hardware can execute natively.

The lowering pipeline typically progresses through multiple intermediate representations (IRs), each with decreasing abstraction. A high-level graph from a framework like PyTorch or TensorFlow is first lowered to a mid-level IR (e.g., MLIR dialects) for hardware-agnostic optimizations, then to a low-level IR for target-specific optimizations, and finally to machine code or vendor-specific kernels. Key transformations during lowering include operator legalization, type legalization, and control flow flattening, ensuring the final graph conforms to the target's execution model and instruction set.

GRAPH COMPILATION STRATEGIES

Core Characteristics of Graph Lowering

Graph lowering is the foundational compiler process that transforms abstract, high-level computational graphs into concrete, hardware-executable instructions through a series of deterministic, rule-based transformations.

01

Multi-Level Abstraction Lowering

Graph lowering operates across multiple levels of abstraction, systematically decomposing high-level operations. It begins with framework-specific operators (e.g., a PyTorch nn.Conv2d) and progresses through several Intermediate Representation (IR) dialects—such as linear algebra, loop nests, and vector operations—before reaching target-specific machine instructions. Each level applies legalization rules to convert operations into a form supported by the next, lower level. This layered approach enables hardware-agnostic high-level optimizations before committing to vendor-specific details.

02

Hardware-Specific Legalization

A core function of lowering is legalization, the process of replacing or decomposing operations unsupported by the target hardware with equivalent, legal sequences. For an NPU, this might involve:

  • Replacing a high-level Softmax with a manual decomposition using Exp, ReduceSum, and Div.
  • Converting a 5D tensor operation into a series of lower-dimensional ops if the hardware only supports up to 4D.
  • Mapping floating-point operations to fixed-point or block floating-point units. The legalizer uses a pattern-matching system to apply these hardware-specific rewriting rules.
03

Static Analysis and Shape Inference

Effective lowering depends on static shape inference, a compile-time analysis that propagates tensor dimension information throughout the graph. By resolving shapes before execution, the compiler can:

  • Perform accurate memory planning, pre-allocating buffers of known size.
  • Enable optimizations like constant folding and dead code elimination.
  • Generate efficient, fixed-loop kernels without dynamic control flow overhead. This analysis transforms a dynamically-shaped graph from training frameworks into a static, predictable dataflow graph suitable for NPU execution.
04

Control Flow Linearization

NPUs and many accelerators excel at data-parallel computation but have limited support for complex control flow like loops and conditionals. A key lowering step is control flow flattening or linearization. This transforms structured control constructs into a dataflow representation using predicated execution or explicit computation graphs with conditional data edges. For example, a tf.while_loop may be unrolled a static number of times or lowered to a state machine where iterations are controlled by explicit data dependencies, eliminating branch instructions.

05

Integration with Optimization Passes

Lowering is not a single pass but a pipeline interleaved with optimization passes. Key optimizations performed during or just before lowering include:

  • Operator Fusion: Merging adjacent nodes (e.g., Conv + BiasAdd + ReLU) into a single compound kernel.
  • Layout Transformation: Converting tensor memory layouts (e.g., NCHW to NHWC) to match hardware access patterns.
  • Common Subexpression Elimination (CSE): Removing duplicate calculations introduced during legalization.
  • Constant Folding: Pre-computing operations on static weights. These optimizations are applied at the most appropriate abstraction level, often immediately after a legalization step creates new optimization opportunities.
06

Target-Specific Code Generation

The final stage of lowering is code generation, where the lowest-level IR is converted into executable code for the target. This involves:

  • Instruction Selection: Mapping IR ops to specific NPU instructions (e.g., a MAC operation to a systolic array instruction).
  • Register Allocation: Assigning virtual tensors to physical hardware registers or memory buffers.
  • Scheduling: Determining the temporal order of instruction issuance to hide memory latency. This stage heavily utilizes the Vendor SDK and Intrinsic Mapping, emitting code that may be C/C++, assembly, or a proprietary binary format (e.g., TensorRT engine, CoreML mlmodel).
GRAPH COMPILATION STRATEGIES

How Graph Lowering Works: The Step-by-Step Process

Graph lowering is the systematic transformation of a high-level computational graph into a hardware-executable form, a core process within NPU compilation.

Graph lowering is the compiler process of transforming a high-level, abstract computational graph into a lower-level, hardware-specific representation through a series of legalization and conversion passes. The goal is to bridge the semantic gap between a framework-defined model and the NPU's instruction set architecture (ISA), enabling efficient execution. This involves decomposing complex, generic operations into sequences of primitive operations that the target hardware natively supports.

The process typically follows a multi-stage lowering pipeline. First, a frontend converts a model from a framework like PyTorch into a common Intermediate Representation (IR). Subsequent passes then perform operator legalization, replacing unsupported ops with equivalent subgraphs. Finally, backend lowering maps the legalized graph to specific hardware instructions, registers, and memory addresses, producing an optimized binary for the NPU. This ensures the computational intent is preserved while maximizing hardware utilization.

COMPILER TRANSFORMATION

Levels of Abstraction in a Lowering Pipeline

This table illustrates the progressive transformation of a computational graph from a high-level, framework-specific representation down to hardware-specific machine code, detailing the key characteristics and operations at each stage.

Abstraction LevelRepresentation FormKey CharacteristicsPrimary Compiler Operations

Framework Graph (e.g., PyTorch, TensorFlow)

High-Level Operator Graph

Framework-specific operators (e.g., nn.Conv2d, tf.matmul). Dynamic shapes possible. Rich operator semantics and attributes.

Import from framework. Initial shape inference. High-level algebraic simplifications.

Canonicalized Graph

Normalized Operator Graph

Syntactic variations eliminated. Operators lowered to a canonical set (e.g., decompose complex ops). Control flow may be represented explicitly.

Graph canonicalization. Operator decomposition. Static shape inference (where possible).

Lowered Graph (Mid-Level IR)

Target-Agnostic Computational Graph

Hardware-agnostic, pure compute/dataflow. Often in Static Single Assignment (SSA) form. Explicit memory buffers. Loops and conditionals represented as primitive control flow nodes.

Common subexpression elimination. Dead code elimination. Memory planning. Loop-invariant code motion. Early fusion planning.

Hardware-Specific Graph

Vendor/Architecture IR

Operations mapped to target hardware primitives (e.g., NPU tensor cores, DMA ops). Explicit memory hierarchy (global, shared, local). Vendor-specific intrinsics and data layouts appear.

Operator lowering/lowering to intrinsics. Layout transformations. Graph partitioning for multi-core. Instruction selection.

Scheduled Kernel Graph

Scheduled Compute + Memory Ops

Explicit parallelism (workgroups, warps, wavefronts). Synchronization primitives inserted. Memory movement (load/store, DMA) explicitly scheduled relative to compute.

Loop tiling and unrolling. Parallelism mapping (thread/block assignment). Software pipelining. Insertion of barrier operations.

Low-Level IR (e.g., LLVM IR, SPIR-V)

Linearized Instruction Stream

Register-based operations. Low-level control flow (branches, labels). Virtual or physical register allocation. Approaching general-purpose ISA level.

Register allocation. Peephole optimizations. Machine-specific instruction combining. Final instruction scheduling.

Machine Code / Microcode

Binary Executable

Hardware-specific instructions (ISA). Absolute addresses. Configured hardware registers. May include proprietary microcode sequences for fixed-function units.

Binary assembly and linking. Address resolution. Generation of metadata and dispatch headers for the NPU runtime.

IMPLEMENTATION PATTERNS

Graph Lowering in Major Frameworks & Compilers

Graph lowering is the systematic process of transforming a high-level, abstract computational graph into a lower-level, hardware-specific representation. This section details how major frameworks and compilers implement this critical phase of the compilation pipeline.

01

TensorFlow / XLA: HLO to LLO

The XLA (Accelerated Linear Algebra) compiler within TensorFlow performs graph lowering through a defined sequence of Intermediate Representations (IRs). The process begins with the HLO (High-Level Optimizer) IR, which represents the graph after framework-level optimizations. XLA then lowers HLO through multiple passes:

  • Target-specific legalization converts unsupported HLO ops to supported ones.
  • Buffer assignment allocates memory for all tensors.
  • Lowering to LLO (Low-Level Optimizer) IR, which is closer to machine code.
  • Final lowering to LLVM IR for CPU/GPU or proprietary TPU IR for Google's hardware. This multi-stage approach allows for both hardware-agnostic and target-specific optimizations.
02

PyTorch / TorchInductor: Aten to Triton/OpenMP

PyTorch's TorchInductor compiler, used by torch.compile, lowers graphs via the Aten IR (PyTorch's core tensor operation library). The lowering path is determined by the target backend:

  • For NVIDIA GPUs, it lowers Aten ops to Triton IR, a high-level Python-like language for GPU programming, which is then compiled to PTX.
  • For CPUs, it lowers Aten to OpenMP-annotated C++ loops via the Cpp backend.
  • The process involves operator decomposition, where complex Aten ops are broken down into simpler primitives, followed by fusion and scheduling on the target IR. This design prioritizes flexibility and performance on diverse hardware.
03

Apache TVM: Relay to TIR

TVM (Tensor Virtual Machine) employs a clear separation between computational and scheduling concerns. Graph lowering flows from Relay, a high-level functional IR for neural networks, down to TIR (Tensor IR).

  • The Relay → TIR lowering involves legalization (mapping Relay operators to TIR primitives) and memory planning.
  • TIR is a low-level, explicit loop-nest IR that represents detailed computation and hardware-aware schedules (e.g., loop tiling, vectorization).
  • Finally, TIR is lowered to target code via code generation for backends like LLVM, CUDA, Vulkan, or vendor-specific NPU SDKs. This modularity is key to TVM's support for a wide array of accelerators.
04

MLIR: Dialect Lowering

MLIR (Multi-Level Intermediate Representation) formalizes graph lowering as dialect lowering. A computational graph enters MLIR in a high-level dialect (e.g., tosa, mhlo).

  • Lowering is a series of conversion passes that translate operations from one dialect to another, progressively moving from abstract to concrete representations (e.g., mhlolinalgaffinellvm).
  • Each dialect owns a set of operations, types, and transformations. The pattern rewrite engine applies lowering patterns to convert ops.
  • This allows hardware vendors to define their own dialects (e.g., npu for a specific NPU) and lowering paths, creating a customizable and interoperable compilation stack.
05

ONNX Runtime: Graph Transformations & EP

ONNX Runtime lowers ONNX model graphs for execution via a combination of graph transformations and Execution Provider (EP) interfaces.

  • First, graph optimization passes (constant folding, node fusion) are applied on the ONNX graph.
  • The optimized graph is then partitioned and assigned to hardware-specific EPs (e.g., CUDA EP, TensorRT EP, OpenVINO EP).
  • Each EP is responsible for the final lowering of its subgraph. For example, the CUDA EP may convert ONNX nodes to CUDA kernel calls, while the TensorRT EP performs a more aggressive lowering and fusion to TensorRT's engine format. This architecture decouples high-level optimizations from vendor-specific lowering.
06

Vendor NPU SDKs (e.g., Qualcomm, NVIDIA)

Vendor SDKs for NPUs (like Qualcomm's AI Engine Direct or NVIDIA's TensorRT) perform the final, most hardware-specific stage of graph lowering.

  • They ingest a framework-exported graph (e.g., ONNX, TensorFlow Lite) and apply vendor-optimized graph passes.
  • This includes kernel selection (choosing the most efficient pre-written kernel for each op), memory allocation aligned with the NPU's memory hierarchy, and scheduling onto the NPU's cores.
  • The output is a serialized engine or binary containing instructions and data tightly coupled to the NPU's microarchitecture. This stage is where abstract tensor operations become concrete hardware instructions.
GRAPH LOWERING

Frequently Asked Questions

Graph lowering is a foundational compiler technique for deploying machine learning models. These questions address its core mechanisms, practical applications, and relationship to other optimization strategies.

Graph lowering is the systematic process of transforming a high-level, abstract computational graph representation into a lower-level, hardware-specific representation through a series of legalization and conversion passes. It works by applying a sequence of rewrite rules and lowering patterns that incrementally replace abstract operations with concrete implementations. For example, a single conv2d operation in a framework like PyTorch might be lowered through several intermediate representations (IRs): first to a set of linear algebra primitives (like matmul and bias_add), then to vendor-specific intrinsics (like NVIDIA's Tensor Cores or an NPU's matrix multiplication unit), and finally to scheduled machine instructions. Each step makes implicit hardware constraints explicit, enabling subsequent optimizations like kernel fusion and memory planning.

Prasad Kumkar

About the author

Prasad Kumkar

CEO & MD, Inference Systems

Prasad Kumkar is the CEO & MD of Inference Systems and writes about AI systems architecture, LLM infrastructure, model serving, evaluation, and production deployment. Over 5+ years, he has worked across computer vision models, L5 autonomous vehicle systems, and LLM research, with a focus on taking complex AI ideas into real-world engineering systems.

His work and writing cover AI systems, large language models, AI agents, multimodal systems, autonomous systems, inference optimization, RAG, evaluation, and production AI engineering.