Skip to guide

One architecture, many systems

Scalable Verification Infrastructure

Scale the same verification intent across protocol variants, large agent counts, global commands, emulation transactors, and high-volume metrics.

Separate scenario intent from physical signaling, centralize topology and broadcast state, and aggregate evidence without burying simulation in logs.

2 connected case studiesFailure → evidence → recoveryMechanism diagrams, code, stress, and Q&A
One architecture, many systemsOne reusable contract across 2 cases.
  1. 01INTENTKeep scenarios independent of one physical interface
  2. 02BINDResolve topology, protocol, and implementation policy
  3. 03OBSERVEAggregate identity-preserving evidence
  4. 04REUSERun across simulation, acceleration, and new systems

Polymorphic TB · Performance DV

01 · Reusable infrastructure

Dynamic Reconfiguration & Polymorphic Testbenches

Keep one stable verification environment while swapping protocol implementations, lane-width variants, software models, or emulator transactors behind a deliberate abstract contract.

Mechanism · failure · evidence · recovery
Stable contract, swappable implementationReuse comes from an honest abstraction boundary.

A common agent contract owns intent and analysis data; factory-selected concrete implementations adapt protocol, lane width, software model, or emulation transport before build creates them.

Stable environment contractvirtual driver API + canonical transactionconfigure before create()
AXI agent64-bit RTL interfaceCHI agentflit-based interfaceDPI modelshared C implementationEmulation proxyaccelerated transactor
Invariantanalysis schemaVarianttransport + timingGuardrailcompatible interface type

override before factory create · concrete adapters preserve canonical transaction semantics

Why it matters

  • One IP can support several protocols, such as a SerDes configured for PCIe, SATA, or USB.
  • The same design can have structural variants, such as 4-lane and 16-lane configurations.
  • A polymorphic testbench keeps tests and environment connections stable while changing Driver, Monitor, model, or transactor implementation.

What is difficult

  • A factory override can replace a PCIe_Driver with a SATA_Driver, but their virtual interfaces may have different signals and protocol semantics.
  • Every implementation must satisfy a stable base API while still reaching its protocol-specific interface.
  • Factory overrides must be installed before the requested component is instantiated.
  • Excessive runtime substitution can hide which class is executing and make the environment difficult for a new engineer to debug.

Failure signatures

  • An override is installed after creation and has no effect.
  • The environment calls new() directly and bypasses the factory.
  • The selected driver cannot use the configured virtual-interface type.
  • A protocol_t topology setting disagrees with the factory-selected implementation.
  • A generic transaction cannot be translated into required protocol-specific signal behavior.
  • The source visible in the environment differs from the runtime class and obscures debug ownership.

Two viable approaches—and their cost

UVM factory overrides

Use set_type_override_by_type to replace a base component with an extended implementation.

Strengths
  • Clean, standard UVM selection at the test level.
  • Leaves the reusable environment unchanged.
Costs
  • All replacements must share an assignment-compatible base class.
  • Factory substitution does not solve signal-level interface differences.

Abstract or facade class

Give the environment an abstract-driver contract and provide the concrete implementation through factory/configuration policy.

Strengths
  • Decouples the environment from physical signaling.
  • Can hide an SV driver, C++ DPI model, or Palladium emulator transactor behind one API.
Costs
  • Needs a bridge from generic UVM transactions to protocol-specific operations.
  • The shared API can become too broad if every protocol-specific exception leaks into it.

Interview answer, built from the mechanism

  1. For a multi-protocol IP, I define abstract base Drivers and Monitors that specify what the environment can request and observe. Derived classes implement how PCIe, SATA, USB, or an emulator transactor performs that work.
  2. The test installs a UVM factory type override before the environment creates the base component. A typed configuration object carries protocol_t and the topology or interface information needed by that implementation.
  3. This lets the same test scenario and environment connection logic run against different protocol and lane-width variants while keeping physical signaling behind a bridge or facade.
  4. I also print or record the resolved factory type and topology because runtime polymorphism must remain observable during debug.

Component responsibility contract

Component responsibilities and required verification changes for Dynamic Reconfiguration & Polymorphic Testbenches
ComponentResponsibilityRequired change
Base DriverInterface definitionCreate an abstract class with a pure virtual drive_bus() task.
TestSelectorInstall the factory type override before the target build-phase creation.
Config ObjectTopology mapAdd a protocol_t enum field that guides build and interface policy.

Implementation patterns

Base contract and PCIe overridesystemverilog
// --- The Abstract Base ---
virtual class base_driver extends uvm_driver #(trans);
  pure virtual task drive_bus(trans tr);
endclass

// --- Specialized Implementation ---
class pcie_driver extends base_driver;
  `uvm_component_utils(pcie_driver)

  virtual task drive_bus(trans tr);
    /* PCIe Handshake */
  endtask
endclass

// --- The Test (Swapping implementation) ---
class pcie_test extends base_test;
  virtual function void build_phase(uvm_phase phase);
    // Replace generic base with specific PCIe version
    set_type_override_by_type(
      base_driver::get_type(),
      pcie_driver::get_type()
    );
    super.build_phase(phase);
  endfunction
endclass

The environment requests base_driver. The test changes the factory resolution before super.build_phase() creates the environment, while pcie_driver supplies the protocol-specific handshake.

Stress recipe

  1. Run one unchanged test with the base request resolved to pcie_driver.
  2. Select a SATA or USB implementation through the same base API and confirm that environment connections do not change.
  3. Repeat across 4-lane and 16-lane topology settings and verify protocol_t matches the bound interface.
  4. Install an override after creation in a negative test and prove that the existing component does not change.
  5. Exercise a protocol-specific signal requirement to verify that the wrapper, bridge, or typed VIF reaches the selected agent.
  6. Perform a mid-test behavior change only through an explicit Mux or Proxy and check that ownership remains deterministic.

Follow-up questions

How do you handle different interfaces for different drivers?

Use a wrapper interface containing the required signal families, or pass the specific typed virtual interface through the factory-overridden agent. Keep protocol translation behind the facade rather than exposing it throughout the environment.

Can you override a component after build_phase?

No factory override can replace an instance that already exists. Install the override before creation. Use a Mux component or Proxy pattern when behavior truly must switch during a test.

What is the risk of excessive polymorphism?

The testbench becomes a black box: the class visible in the environment source is not the class executing at runtime, making ownership and debug harder to follow.

02 · Performance architecture

Performance Verification Framework

Measure transaction lifecycle, latency distributions, tail behavior, bandwidth, stalls, and effective throughput without turning the simulator log into the bottleneck.

Mechanism · failure · evidence · recovery
Distribution, not one averageMake the long tail visible.

Lifecycle timestamps feed an in-memory histogram, a sliding bandwidth window, and separate average, p99, and maximum results.

start0 nsaccept18 nsdone146 ns
0–10 nsp99max
Average42 nsP99 tail138 nsMaximum211 nsSustained BWbits / window time

Why it matters

  • Functional correctness is only the baseline. A fabric must also meet throughput, stall, latency, and gigabyte-per-second architecture targets.
  • Average latency alone can hide the 99th-percentile tail where architectural bottlenecks appear.
  • Timestamped transactions and a reusable subscriber turn performance into a measurable verification result rather than an informal waveform inspection.

What is difficult

  • Tracking every cycle of every transaction across 100 masters can produce enough data to slow simulation by 5x to 10x.
  • Recording only 1 out of 100 transactions reduces overhead but can miss rare tail-latency spikes.
  • Keeping all statistics in memory avoids log bloat, but a simulation crash can lose data that was never persisted.
  • Bandwidth, backpressure response, skid depth, and effective throughput require explicit windows and boundary definitions.

Failure signatures

  • The design passes functionally but misses sustained bandwidth or latency targets.
  • Statistical sampling misses a rare 99th-percentile stall.
  • Per-transaction logging dominates runtime and distorts the workload being measured.
  • In-memory histograms disappear when simulation terminates unexpectedly.
  • READY/VALID backpressure leaves extra data in flight beyond the intended skid depth.
  • Protocol headers and idle gaps consume cycles, reducing useful effective throughput.

Two viable approaches—and their cost

Statistical sampling monitor

Record timing for 1 out of every 100 transactions.

Strengths
  • Very low collection overhead.
  • Can estimate average latency when sampling is unbiased and sufficiently large.
Costs
  • Can miss corner-case and 99th-percentile latency spikes.
  • Sampling policy can bias results during bursty or priority-dependent traffic.

In-memory analysis subscriber

Process every transaction in memory and emit only a final summary table or machine-readable report.

Strengths
  • Maintains full transaction-count accuracy.
  • Avoids per-event log-file bloat.
Costs
  • Unflushed data is lost if simulation crashes.
  • Histogram state can still consume substantial memory in a long, high-cardinality run.

Interview answer, built from the mechanism

  1. I build a performance framework that records start, accept, and done timestamps in each monitored transaction.
  2. A UVM subscriber computes latency and aggregates it into in-memory histograms rather than printing every event. That preserves simulation throughput while retaining the distribution.
  3. I report average latency and the 99th-percentile tail, then export CSV for comparison with the architectural golden model.
  4. I also define a sliding bandwidth window, separate peak from sustained bandwidth, measure backpressure/skid response, and report effective throughput as useful data beats divided by total clock cycles.

Component responsibility contract

Component responsibilities and required verification changes for Performance Verification Framework
ComponentResponsibilityRequired change
TransactionTimestampsAdd realtime fields for start, accept, and done.
SubscriberStats engineUse an associative array to bin latencies into a histogram.
Final ReportCSV/SQL outputDump data in a format that Python or Excel can parse.

Implementation patterns

10 ns latency histogramsystemverilog
class perf_analyzer extends uvm_subscriber #(trans);
  int latency_bins[int]; // 0-10ns, 10-20ns, ...

  virtual function void write(trans t);
    realtime lat = t.done_time - t.start_time;
    int bin = lat / 10ns;
    latency_bins[bin]++;
  endfunction

  virtual function void report_phase(uvm_phase phase);
    foreach (latency_bins[i])
      `uvm_info(
        "PERF",
        $sformatf(
          "Bin %0dns-%0dns: %0d hits",
          i * 10,
          (i + 1) * 10,
          latency_bins[i]
        ),
        UVM_LOW
      )
  endfunction
endclass

Each transaction contributes done_time minus start_time to one 10 ns bucket. The report prints every occupied interval and its hit count.

Performance formulas and boundariestext
Source rendering:
BW=Window_TimeBits_Transferred

Intended dimensional form:
bandwidth = bits_transferred / window_time

effective_throughput =
  useful_data_beats / total_clock_cycles

backpressure observation =
  cycles from READY low to the last VALID beat

The source formula is preserved verbatim, then qualified with the dimensionally correct bandwidth relationship. Peak and sustained results must use named window boundaries.

Stress recipe

  1. Drive representative congestion across 100 masters and timestamp start, accept, and done for every observed packet.
  2. Run a full-capture subscriber and a 1-in-100 sampler on the same seeded workload.
  3. Compare average and 99th-percentile latency to quantify sampling error.
  4. Bin full-capture latency in 10 ns increments and export a final CSV or SQL-ready report.
  5. Measure bits transferred in named sliding windows and report peak and sustained bandwidth.
  6. Deassert READY under load, count cycles to the last VALID beat, and compare with the allowed skid depth.
  7. Calculate useful data beats per total clock cycle and attribute lost efficiency to headers, stalls, and idle gaps.

Follow-up questions

How do you measure bandwidth over time?

Use a sliding window, count transferred bits inside it, and divide by the window duration. Track peak separately from sustained bandwidth.

How do you verify backpressure efficiency?

Measure the cycles between READY going low and the last VALID beat. Relate that delay to the permitted skid or buffering depth.

What is effective throughput?

It is the ratio of useful data beats to total clock cycles. It exposes cycles consumed by protocol headers and idle gaps.

Continue the evidence chain

Connect cases to system strategy.

Move from focused failure modes to a complete plan, executable architecture, coverage model, regression system, and sign-off argument.