SOFIE Manager Implementation Guide
See Also:
- ONNX Implementation - Alternative ML backend
- Using SOFIE in Analyses
- SOFIE Configuration
- SOFIE API Reference
Overview
SofieManager enables you to use SOFIE (System for Optimized Fast Inference code Emit) models from ROOT TMVA in RDFAnalyzerCore. SOFIE generates optimized C++ inference code from ONNX models at build time, which eliminates runtime model loading and interpretation overhead.
Key Differences from ONNX/BDT
| Feature | SOFIE | ONNX | BDT |
|---|---|---|---|
| Model Format | C++ code (compiled) | ONNX file (runtime) | Text file (runtime) |
| Loading | Build-time compilation | Runtime file loading | Runtime file loading |
| Performance | No runtime loading overhead | Optimized runtime execution | Tree evaluation |
| Setup | Manual registration | Auto from config | Auto from config |
| Flexibility | Less (rebuild required) | High (swap files) | High (swap files) |
| Best For | Production, finalized models | Development, flexibility | Gradient boosted trees |
When to Use SOFIE
Use SOFIE when:
- Inference performance is important and models are finalized
- Model is stable and won’t change frequently
- You can rebuild between model updates
- You want to eliminate runtime model loading overhead
Use ONNX when:
- Model is still being developed/tuned
- You need to swap models without recompiling
- You want model portability across frameworks
- Runtime flexibility is more important than peak speed
What Was Implemented
1. SofieManager Plugin
Files:
core/plugins/SofieManager/SofieManager.hcore/plugins/SofieManager/SofieManager.cccore/plugins/SofieManager/CMakeLists.txt
Features:
- Manages compiled SOFIE inference functions
- Manual model registration (not auto-loaded from files)
- Conditional execution via
runVar - Thread-safe for use with ROOT’s ImplicitMT
- Same API pattern as BDT/ONNX managers
Key Methods:
registerModel(name, inferenceFunc, features, runVar): Register a SOFIE modelapplyModel(modelName): Apply a specific model to the DataFrameapplyAllModels(): Apply all registered modelsgetModel(key): Retrieve inference functiongetModelFeatures(key): Get input feature namesgetRunVar(modelName): Get conditional run variablegetAllModelNames(): List all registered models
2. Configuration Format
Create a configuration file (e.g., cfg/sofie_models.txt):
name=dnn_score inputVariables=pt,eta,phi,mass runVar=has_jet
name=classifier inputVariables=lep_pt,lep_eta,met runVar=pass_presel
Parameters:
name: Output column name for model predictionsinputVariables: Comma-separated list of input featuresrunVar: Boolean column controlling when model runs
Key Difference: No file parameter - SOFIE models are compiled code, not files.
Add to main config:
sofieConfig=cfg/sofie_models.txt
3. Comprehensive Unit Tests
File: core/test/testSofieManager.cc
Test Coverage:
- Constructor and manager creation
- Model registration
- Model retrieval (valid and invalid)
- Feature retrieval
- RunVar retrieval
- Model application with valid inputs
- Model application with runVar=false
- Multiple model support
- Thread safety with ROOT ImplicitMT
Generating SOFIE Code from ONNX Models
SOFIE requires you to convert your ONNX models to C++ code. This is done using ROOT’s Python interface.
Step 1: Train Your Model
Train your model in any framework and export to ONNX:
# PyTorch example
import torch
model = MyNeuralNetwork()
# ... train model ...
dummy_input = torch.randn(1, num_features)
torch.onnx.export(model, dummy_input, "my_model.onnx",
input_names=['input'],
output_names=['output'],
dynamic_axes={'input': {0: 'batch_size'},
'output': {0: 'batch_size'}})
Step 2: Generate SOFIE C++ Code
Use ROOT’s TMVA SOFIE to generate C++ inference code:
import ROOT
from ROOT import TMVA
# Load your ONNX model
model = TMVA.Experimental.SOFIE.RModelParser_ONNX("my_model.onnx")
# Generate C++ code
model.Generate()
# Output to a header file
model.OutputGenerated("MyModel.hxx")
This creates MyModel.hxx containing:
namespace TMVA_SOFIE_MyModelSessionclass withinfer()method- All necessary tensor operations in C++
Step 3: Inspect Generated Code
The generated header contains everything needed for inference:
// MyModel.hxx (simplified)
namespace TMVA_SOFIE_MyModel {
class Session {
private:
std::vector<float> weights_; // Model weights
// ... other internal state ...
public:
Session(); // Constructor initializes weights
std::vector<float> infer(const float* input_data);
};
} // namespace TMVA_SOFIE_MyModel
Using SOFIE Models in Your Analysis
Step 1: Include Generated Headers
In your analysis code:
#include "MyModel.hxx" // SOFIE-generated code
#include <SofieManager.h>
Step 2: Create Wrapper Function
Create a wrapper matching the SofieInferenceFunction signature:
std::vector<float> myModelInference(const std::vector<float>& input) {
// Create SOFIE session (lightweight, can be static)
static TMVA_SOFIE_MyModel::Session session;
// Run inference
std::vector<float> output = session.infer(input.data());
return output;
}
Performance Tip: Make the session static to avoid re-initialization.
Step 3: Register the Model
Register your model with SofieManager:
#include <SofieManager.h>
// Create manager
auto sofieManager = std::make_unique<SofieManager>(*configProvider);
ManagerContext ctx{*configProvider, *dataManager, *systematicManager,
*logger, *skimSink, *metaSink};
sofieManager->setContext(ctx);
// Create inference function
auto inferenceFunc = std::make_shared<SofieInferenceFunction>(myModelInference);
// Register model
std::vector<std::string> features = {"pt", "eta", "phi", "mass"};
sofieManager->registerModel("my_model", inferenceFunc, features, "has_jet");
Step 4: Define Input Variables
Define all required input features before applying models:
// Define input features
analyzer.Define("pt", [](float x) { return x; }, {"jet_pt"});
analyzer.Define("eta", [](float x) { return x; }, {"jet_eta"});
analyzer.Define("phi", [](float x) { return x; }, {"jet_phi"});
analyzer.Define("mass", [](float x) { return x; }, {"jet_mass"});
// Define run variable
analyzer.Define("has_jet",
[](int n_jets) { return n_jets > 0; },
{"n_jets"});
Step 5: Apply Models
Apply the registered models:
// Apply specific model
sofieManager->applyModel("my_model");
// Or apply all registered models
sofieManager->applyAllModels();
Step 6: Use Model Output
The model output is now available as a DataFrame column:
// Use in event selection
analyzer.Filter("ml_cut",
[](float score) { return score > 0.7; },
{"my_model"});
// Use in variable definition
analyzer.Define("event_weight",
[](float gen_weight, float ml_score) {
return gen_weight * ml_score;
},
{"genWeight", "my_model"});
Complete Example
1. Generate SOFIE Code (Python)
import ROOT
from ROOT import TMVA
# Load ONNX model
model = TMVA.Experimental.SOFIE.RModelParser_ONNX("classifier.onnx")
# Generate C++ code
model.Generate()
model.OutputGenerated("ClassifierModel.hxx")
print("Generated ClassifierModel.hxx")
2. Analysis Code (C++)
#include <analyzer.h>
#include <SofieManager.h>
#include "ClassifierModel.hxx" // SOFIE-generated
// Wrapper function
std::vector<float> classifierInference(const std::vector<float>& input) {
static TMVA_SOFIE_ClassifierModel::Session session;
return session.infer(input.data());
}
int main(int argc, char** argv) {
// Create analyzer
Analyzer analyzer(argv[1]);
// Create SOFIE manager
auto sofieMgr = std::make_unique<SofieManager>(*analyzer.getConfigProvider());
ManagerContext ctx{
*analyzer.getConfigProvider(),
*analyzer.getDataManager(),
*analyzer.getSystematicManager(),
*analyzer.getLogger(),
*analyzer.getSkimSink(),
*analyzer.getMetaSink()
};
sofieMgr->setContext(ctx);
// Register model
auto inferenceFunc = std::make_shared<SofieInferenceFunction>(classifierInference);
std::vector<std::string> features = {"jet_pt", "jet_eta", "jet_phi", "jet_mass"};
sofieMgr->registerModel("classifier", inferenceFunc, features, "has_jet");
// Define input variables
analyzer.Define("jet_pt", ...);
analyzer.Define("jet_eta", ...);
analyzer.Define("jet_phi", ...);
analyzer.Define("jet_mass", ...);
analyzer.Define("has_jet",
[](int n) { return n > 0; },
{"n_jets"});
// Apply model
sofieMgr->applyModel("classifier");
// Use output
analyzer.Filter("classifier_cut",
[](float score) { return score > 0.8; },
{"classifier"});
// Save
analyzer.save();
return 0;
}
3. Configuration (cfg/sofie_models.txt)
name=classifier inputVariables=jet_pt,jet_eta,jet_phi,jet_mass runVar=has_jet
4. Main Config (cfg/analysis.txt)
fileList=data.root
saveFile=output.root
sofieConfig=cfg/sofie_models.txt
threads=-1
Advanced Usage
Multiple Models
Register and use multiple SOFIE models:
// Register multiple models
auto model1Func = std::make_shared<SofieInferenceFunction>(model1Inference);
sofieMgr->registerModel("tagger", model1Func, {"features1"}, "run_tagger");
auto model2Func = std::make_shared<SofieInferenceFunction>(model2Inference);
sofieMgr->registerModel("discriminator", model2Func, {"features2"}, "run_disc");
// Apply all
sofieMgr->applyAllModels();
// Use outputs
analyzer.Define("combined_score",
[](float tag, float disc) { return tag * disc; },
{"tagger", "discriminator"});
Model with Multiple Outputs
SOFIE models can return multiple outputs:
std::vector<float> multiOutputInference(const std::vector<float>& input) {
static TMVA_SOFIE_MultiOutput::Session session;
// Session returns vector with multiple values
return session.infer(input.data());
}
// Register (outputs go to single column as vector)
sofieMgr->registerModel("multi_model", inferenceFunc, features, runVar);
// Access outputs
analyzer.Define("output0",
[](const std::vector<float>& outputs) {
return outputs.size() > 0 ? outputs[0] : -1.0f;
},
{"multi_model"});
analyzer.Define("output1",
[](const std::vector<float>& outputs) {
return outputs.size() > 1 ? outputs[1] : -1.0f;
},
{"multi_model"});
Conditional Execution
Skip expensive inference when not needed:
// Define complex condition
analyzer.Define("run_expensive_model",
[](int n_jets, float met, bool pass_presel) {
return pass_presel && n_jets >= 4 && met > 50.0;
},
{"n_jets", "met", "pass_preselection"});
// Model only runs when condition is true
sofieMgr->registerModel("expensive_model", func, features, "run_expensive_model");
When runVar is false, output is -1.0 (consistent with ONNX/BDT managers).
Build System Integration
Including Generated Headers
Add the directory containing your SOFIE headers to include paths:
# In your analysis CMakeLists.txt
target_include_directories(myanalysis
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/sofie_models # Where .hxx files live
)
Directory Structure
MyAnalysis/
├── CMakeLists.txt
├── analysis.cc
├── sofie_models/ # SOFIE generated headers
│ ├── Classifier.hxx
│ └── Discriminator.hxx
├── cfg/
│ ├── analysis.txt
│ └── sofie_models.txt
└── models/ # Original ONNX files (for reference)
├── classifier.onnx
└── discriminator.onnx
Limitations
- Manual Registration Required
- Unlike ONNX/BDT, models aren’t auto-loaded from config
- Must write wrapper functions and call
registerModel()
- Rebuild Required for Updates
- Changing models requires regenerating C++ code
- Must rebuild analysis
- Less flexible than runtime loading
- ONNX as Intermediate
- Must start with ONNX model
- SOFIE generates from ONNX
- Cannot directly use other formats
- Single Scalar Output Per Model (Current)
- Multi-output supported at SOFIE level
- Manager returns first output or vector
- Extract multiple outputs manually if needed
Troubleshooting
SOFIE Generation Fails
Problem: ONNX model won’t convert to SOFIE
Solution:
- Check ONNX opset version (SOFIE supports specific versions)
- Simplify model architecture (some ops not supported)
- Use ONNX simplifier before SOFIE conversion
- Check ROOT/TMVA version
Compilation Errors
Problem: Generated header doesn’t compile
Solution:
- Include directory must be in include paths
- Check for conflicting namespace names
- Ensure ROOT is properly linked
- Verify TMVA availability
Runtime Errors
Problem: Model inference fails or crashes
Solution:
- Verify input feature count matches model
- Check for NaN/Inf in input data
- Ensure features are in correct order
- Test with simple inputs first
Performance Not as Expected
Problem: SOFIE not faster than ONNX
Solution:
- Ensure compiler optimization enabled (
-O3) - Make Session static (avoid re-initialization)
- Profile to find bottlenecks
- Check if feature extraction is the bottleneck
Best Practices
- Version Control ONNX Files
models/ ├── classifier_v1.onnx ├── classifier_v2.onnx # Keep versions └── classifier_latest.onnx - Regenerate on Model Updates
# After retraining python generate_sofie.py source build.sh - Static Sessions
static TMVA_SOFIE_Model::Session session; // Good // vs TMVA_SOFIE_Model::Session session; // Bad (recreates each call) - Test Before Production
- Verify SOFIE output matches ONNX output
- Use same test data for both
- Check numerical precision
- Document Model Generation
# generate_sofie.py """ Generates SOFIE C++ code from ONNX models. Models: - classifier: trained 2024-01-15, 95% accuracy - discriminator: trained 2024-01-20, 92% AUC """
Migration Guide
From ONNX to SOFIE
If you have an ONNX-based analysis and want to switch to SOFIE:
1. Generate SOFIE code from your ONNX model
import ROOT
from ROOT import TMVA
model = TMVA.Experimental.SOFIE.RModelParser_ONNX("my_model.onnx")
model.Generate()
model.OutputGenerated("MyModel.hxx")
2. Replace OnnxManager with SofieManager
Before (ONNX):
auto onnxMgr = std::make_unique<OnnxManager>(*configProvider);
// ... set context ...
onnxMgr->applyAllModels(); // Auto-loads from config
After (SOFIE):
#include "MyModel.hxx"
std::vector<float> myModelInference(const std::vector<float>& input) {
static TMVA_SOFIE_MyModel::Session session;
return session.infer(input.data());
}
auto sofieMgr = std::make_unique<SofieManager>(*configProvider);
// ... set context ...
auto inferenceFunc = std::make_shared<SofieInferenceFunction>(myModelInference);
sofieMgr->registerModel("my_model", inferenceFunc, features, runVar);
sofieMgr->applyAllModels();
3. Update configuration
Before (onnxConfig):
file=models/my_model.onnx name=my_model inputVariables=pt,eta,phi,mass runVar=has_jet
After (sofieConfig):
name=my_model inputVariables=pt,eta,phi,mass runVar=has_jet
# Note: No file= parameter
4. Update build system
# Add SOFIE header directory
target_include_directories(myanalysis
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/sofie_models
)
5. Rebuild and test
source cleanBuild.sh
# Run test to verify outputs match
Comparison with Other Managers
| Feature | SOFIE | ONNX | BDT |
|---|---|---|---|
| Config-driven | Partial (features only) | Full | Full |
| Auto-load models | No (manual registration) | Yes | Yes |
| Runtime flexibility | Low (rebuild required) | High (swap files) | High (swap files) |
| Inference speed | No loading overhead | Optimized runtime | Tree evaluation |
| Memory overhead | None (compiled in) | Model file size | Model file size |
| Supported models | ONNX-convertible | Any ONNX | Gradient boosted trees |
| Thread safety | Yes | Yes | Yes |
| Multi-output | Yes (manual extraction) | Yes (automatic) | No |
| Development ease | Lower (more steps) | Higher (file-based) | Higher (file-based) |
| Production deployment | Excellent | Good | Good |
Future Enhancements
Potential improvements:
- Auto-registration from config - Like ONNX/BDT
- Multi-output automatic splitting - Like ONNX
- Dynamic batch size support - Variable input sizes
- Direct PyTorch/TF conversion - Skip ONNX intermediate
- Model versioning - Track model versions in code
- Benchmark tools - Compare SOFIE vs ONNX performance
See Also
- ONNX Implementation - Alternative ML backend
- Analysis Guide - Using SOFIE in analyses
- Configuration Reference - SOFIE configuration
- API Reference - SofieManager API
- TMVA SOFIE Documentation - ROOT TMVA SOFIE docs
Performance matters? Use SOFIE. Flexibility matters? Use ONNX.