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:
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:
-
Node Mapping Setup:
-
Solution Vector Passed via OsdiSimInfo:
-
Model Reads Voltages Internally:
4. The Eval Function¶
Signature:
Called From: OsdiInstance::evalCore() in osdiinstance.cpp
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:
- Minimum Conductances: OSDI models enforce
gds >= GDSMIN, autodiff doesn't - Limiting Functions:
$limit()in Verilog-A handled by OSDI, not autodiff - 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 |