Skip to content

VACASK OSDI Model Input Handling

This document describes how VACASK interfaces with OSDI compiled models (.so/.dylib files) generated by OpenVAF.

Overview

VACASK loads OSDI shared libraries at runtime and interfaces with them through the OSDI API. This is fundamentally different from openvaf_jax which generates JAX functions - VACASK uses the native compiled code directly.

1. OSDI Library Loading

Files: VACASK/lib/dynload.cpp, VACASK/lib/osdifile.cpp

OsdiFile::open()
  └─> openDynamicLibrary(filename)
      ├─> dlopen(filename, RTLD_NOW) [Linux/macOS]
      └─> LoadLibrary(filename) [Windows]

Symbols Retrieved from Library: - OSDI_DESCRIPTORS - Array of device descriptors - OSDI_NUM_DESCRIPTORS - Number of devices - OSDI_VERSION_MAJOR/MINOR - API version - OSDI_LIM_TABLE - Limit function table - osdi_log - Logging callback

2. Model and Instance Parameters

Parameter Access Architecture:

Parameters are accessed through a callback function pointer in the descriptor:

void *(*access)(void *inst, void *model, uint32_t id, uint32_t flags)

Parameter Types: - Model Parameters: Shared across all instances (e.g., process corners) - Instance Parameters: Per-device (e.g., W, L) - Output Variables (OpVars): Read-only computed values

Access Flags: - ACCESS_FLAG_READ (0) - Read parameter - ACCESS_FLAG_SET (1) - Set parameter - ACCESS_FLAG_INSTANCE (4) - Instance vs model parameter

Parameter Storage: - Model data: allocated at descriptor->model_size bytes - Instance data: allocated at descriptor->instance_size bytes - Parameters stored at offsets within these structures

3. Voltage Provision to Models

Key Insight: Voltages are NOT passed as function arguments. Instead:

  1. Node Mapping Setup:

    auto nodeMapping = nodeMappingArray();  // at descriptor->node_mapping_offset
    nodeMapping[i] = nodes_[i]->unknownIndex();  // circuit matrix index
    

  2. Solution Vector Passed via OsdiSimInfo:

    simInfo.prev_solve = evalSetup->oldSolution;  // entire circuit solution
    

  3. Model Reads Voltages Internally:

    // Inside OSDI model (generated by OpenVAF)
    voltage_at_node_i = prev_solve[nodeMapping[i]];
    

4. The Eval Function

Signature:

uint32_t (*eval)(void *handle, void *inst, void *model, OsdiSimInfo *info)

Called From: OsdiInstance::evalCore() in osdiinstance.cpp

auto evalFlags = descr->eval(&handle, core(), model_->core(), &simInfo);

OsdiSimInfo Structure:

typedef struct OsdiSimInfo {
    OsdiSimParas paras;      // gmin, tnom, scale, iteration, etc.
    double abstime;          // Current simulation time
    double *prev_solve;      // Circuit solution vector (node voltages!)
    double *prev_state;      // Previous internal state
    double *next_state;      // Next internal state
    uint32_t flags;          // What to compute
} OsdiSimInfo;

Calculation Flags: - CALC_RESIST_RESIDUAL - Compute resistive residual (f) - CALC_REACT_RESIDUAL - Compute reactive residual (q) - CALC_RESIST_JACOBIAN - Compute resistive Jacobian (df/dx) - CALC_REACT_JACOBIAN - Compute reactive Jacobian (dq/dx) - ANALYSIS_DC / ANALYSIS_TRAN / ANALYSIS_AC - ENABLE_LIM / INIT_LIM - Limiting control

5. Jacobian Handling

Critical Design: Jacobians are NOT returned by eval(). Instead, matrix pointers are bound beforehand, and the model writes directly to them.

Binding Phase (before iteration):

// From bindCore()
auto jacResistArray = resistiveJacobianPointers();  // at descriptor->jacobian_ptr_resist_offset
for(auto& entry : jacobian_entries) {
    jacResistArray[i] = matResist->valuePtr(row, col);  // Direct matrix pointer!
}

During Eval: The OSDI model writes Jacobian values directly to the bound matrix pointers - no intermediate copies.

Load Phase (after eval):

descr->load_jacobian_resist(core(), model_->core());
descr->load_jacobian_react(core(), model_->core(), factor);

6. Residual Handling

Storage: Residuals are stored in the instance core structure at offsets:

// OsdiNode structure
uint32_t resist_residual_off;   // Offset to resistive residual
uint32_t react_residual_off;    // Offset to reactive residual

Extraction:

for(auto i : device->nonzeroResistiveResiduals()) {
    auto resOff = descr->nodes[i].resist_residual_off;
    auto contrib = *getDataPtr<double*>(core(), resOff);
    // Load to matrix RHS
}

7. Complete Evaluation Flow

1. SETUP PHASE:
   Circuit::setup()
     └─> OsdiDevice::setup()
         └─> OsdiInstance::setup()
             └─> setup_instance(handle, core, model_core, temp)
                 └─> Allocate internal states

2. BIND PHASE (once per matrix structure change):
   OsdiInstance::bindCore()
     └─> Bind Jacobian pointers to matrix entries
     └─> Bind node mapping to circuit unknowns

3. ITERATION LOOP (Newton-Raphson):
   OsdiDevice::evalAndLoad()

   a. Populate OsdiSimInfo:
      ├─> simInfo.paras = {gmin, tnom, scale, iteration, ...}
      ├─> simInfo.abstime = current_time
      ├─> simInfo.prev_solve = circuit_solution_vector  // VOLTAGES HERE!
      ├─> simInfo.prev_state = previous_internal_states
      ├─> simInfo.next_state = next_internal_states
      └─> simInfo.flags = CALC_RESIST_RESIDUAL | CALC_RESIST_JACOBIAN | ...

   b. For each instance:
      eval(&handle, instance_core, model_core, &simInfo)
        └─> Model internally:
            ├─> Reads voltages: V[i] = prev_solve[nodeMapping[i]]
            ├─> Computes currents, charges
            ├─> Writes residuals to instance_core offsets
            ├─> Writes Jacobians DIRECTLY to matrix pointers
            └─> Updates internal states

   c. For each instance:
      loadCore()
        ├─> load_residual_resist() → to matrix RHS
        ├─> load_residual_react() → to matrix RHS
        └─> Jacobians already written by eval!

4. SOLVE and repeat until converged

8. Key Differences from openvaf_jax

Aspect VACASK + OSDI openvaf_jax
Code type Native compiled (.so) JAX Python functions
Voltage input Via prev_solve array + node mapping Direct function arguments
Jacobian output Direct write to matrix pointers Returned as dictionary
Residual output Read from instance core offsets Returned as dictionary
Autodiff Not used - analytical Jacobians Can use JAX autodiff
GPU support CPU only JAX GPU acceleration

9. Implications for GPU Solver

The GPU solver currently uses JAX autodiff for Jacobians, but this misses:

  1. Minimum Conductances: OSDI models enforce gds >= GDSMIN, autodiff doesn't
  2. Limiting Functions: $limit() in Verilog-A handled by OSDI, not autodiff
  3. Analytical Derivatives: OpenVAF computes symbolic derivatives with numerical stability

Solution: Use openvaf_jax to get the same analytical Jacobians that OSDI models provide, rather than relying on autodiff.

10. File References

Purpose VACASK File
Library loading lib/dynload.cpp, lib/osdifile.cpp
Instance evaluation lib/osdiinstance.cpp
Device management lib/osdidevice.cpp
OSDI API definitions include/osdi.h
Parameter access lib/osdimodel.cpp