capability/capability_validator.cpp¶
Namespaces¶
| Name |
|---|
| sgns |
| sgns::sgprocessing Artifact and manifest binary serialization. |
Classes¶
| Name | |
|---|---|
| struct | sgns::sgprocessing::CapabilityValidator::Impl |
Source code¶
#include <capability/capability_validator.hpp>
#include <processingbase/vulkan_init_guard.hpp>
#include <util/sha256.hpp>
#include <ColorFormat.hpp>
#include <DepthFormat.hpp>
#include <ModelFormat.hpp>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <sstream>
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/statvfs.h>
#endif
namespace sgns::sgprocessing
{
// =========================================================================
// PIMPL
// =========================================================================
struct CapabilityValidator::Impl
{
CapabilitySnapshot snapshot;
bool snapshotBuilt = false;
};
// =========================================================================
// Anonymous-namespace helpers
// =========================================================================
namespace
{
std::string JoinStrings( const std::vector<std::string> &items, const std::string &sep )
{
std::ostringstream oss;
for ( size_t i = 0; i < items.size(); ++i )
{
if ( i > 0 ) oss << sep;
oss << items[i];
}
return oss.str();
}
std::string FormatBytes( uint64_t bytes )
{
const char *units[] = { "B", "KB", "MB", "GB", "TB" };
int unit = 0;
double val = static_cast<double>( bytes );
while ( val >= 1024.0 && unit < 4 ) { val /= 1024.0; ++unit; }
char tmp[64];
if ( unit == 0 )
std::snprintf( tmp, sizeof( tmp ), "%llu %s",
static_cast<unsigned long long>( bytes ), units[unit] );
else
std::snprintf( tmp, sizeof( tmp ), "%.1f %s", val, units[unit] );
return tmp;
}
std::string PassTypeToString( PassType pt )
{
switch ( pt )
{
case PassType::COMPUTE: return "COMPUTE";
case PassType::DATA_TRANSFORM: return "DATA_TRANSFORM";
case PassType::INFERENCE: return "INFERENCE";
case PassType::RENDER: return "RENDER";
case PassType::RETRAIN: return "RETRAIN";
}
return std::to_string( static_cast<int>( pt ) );
}
std::string ListAvailablePassTypes( const std::vector<ExecutorCapability> &caps )
{
std::vector<std::string> names;
for ( const auto &cap : caps )
names.push_back( PassTypeToString( cap.passType ) + " ("
+ std::to_string( static_cast<int>( cap.passType ) ) + ")" );
if ( names.empty() ) return "";
return JoinStrings( names, ", " );
}
uint64_t QueryAvailableDiskBytes( const std::string &path )
{
#ifdef _WIN32
ULARGE_INTEGER freeBytesAvailable;
if ( GetDiskFreeSpaceExA( path.empty() ? "." : path.c_str(),
&freeBytesAvailable, nullptr, nullptr ) )
return freeBytesAvailable.QuadPart;
return 0;
#else
struct statvfs stat;
if ( statvfs( path.empty() ? "." : path.c_str(), &stat ) == 0 )
return static_cast<uint64_t>( stat.f_bavail ) * stat.f_frsize;
return 0;
#endif
}
uint64_t BytesPerPixel( sgns::ColorFormat fmt )
{
switch ( fmt )
{
case sgns::ColorFormat::RGBA8: return 4;
case sgns::ColorFormat::RGB8: return 3;
default: return 4;
}
}
uint64_t BytesPerPixel( sgns::DepthFormat fmt )
{
switch ( fmt )
{
case sgns::DepthFormat::D32_SFLOAT: return 4;
case sgns::DepthFormat::D24_UNORM_S8_UINT: return 4;
default: return 4;
}
}
std::vector<ExecutorCapability> CollectExecutorCapabilities(
const std::unordered_map<PassType,
std::function<std::unique_ptr<ProcessingProcessor>()>,
PassTypeHash> &passFactories,
size_t /*mnnProcessorCount*/ )
{
std::vector<ExecutorCapability> caps;
for ( const auto &[passType, factory] : passFactories )
{
(void)factory;
ExecutorCapability cap;
cap.passType = passType;
if ( passType == PassType::RENDER )
{
cap.backend = "VULKAN";
}
else
{
cap.backend = "VULKAN";
cap.supportedModelFormats = { "MNN" };
cap.supportedQuantizations = { "FP32", "FP16", "INT8" };
}
caps.push_back( std::move( cap ) );
}
return caps;
}
const ExecutorCapability *FindExecutorCap( const CapabilitySnapshot &snap,
PassType pt )
{
for ( const auto &cap : snap.executorCaps )
if ( cap.passType == pt ) return ∩
return nullptr;
}
std::string DeriveExecutorId( const std::vector<uint8_t> &identityHash )
{
if ( identityHash.empty() ) return "sgproc-0000000000000000";
std::ostringstream oss;
oss << "sgproc-";
size_t n = (std::min)( size_t( 8 ), identityHash.size() );
for ( size_t i = 0; i < n; ++i )
{
char hex[3];
std::snprintf( hex, sizeof( hex ), "%02x", identityHash[i] );
oss << hex;
}
return oss.str();
}
std::string ModelFormatToString( sgns::ModelFormat fmt )
{
switch ( fmt )
{
case sgns::ModelFormat::MNN: return "MNN";
case sgns::ModelFormat::ONNX: return "ONNX";
case sgns::ModelFormat::PY_TORCH: return "PY_TORCH";
case sgns::ModelFormat::TENSOR_FLOW: return "TENSOR_FLOW";
default: return "UNKNOWN";
}
}
} // anonymous namespace
// =========================================================================
// Construction / destruction
// =========================================================================
CapabilityValidator::CapabilityValidator()
: m_impl( std::make_unique<Impl>() ) {}
CapabilityValidator::~CapabilityValidator() = default;
// =========================================================================
// BuildSnapshot (D-09, D-10, D-11, D-12, D-16)
// =========================================================================
void CapabilityValidator::BuildSnapshot(
const std::unordered_map<PassType,
std::function<std::unique_ptr<ProcessingProcessor>()>,
PassTypeHash> &passFactories,
size_t mnnProcessorCount,
std::function<VkPhysicalDevice()> ensureVulkanDevice )
{
CapabilitySnapshot snapshot;
// Vulkan device query (D-10, D-14)
// NOTE: ensureVulkanDevice() internally calls RenderProcessor::InitializeContext(),
// which acquires VulkanInitMutex() itself via a double-check locking pattern.
// Holding the mutex here while calling ensureVulkanDevice() would cause a
// self-deadlock on the same thread. Only lock around the vkGetPhysicalDevice*
// queries — the device is kept alive by the static RenderProcessor inside the
// lambda, so it's safe to read its properties without the mutex.
{
VkPhysicalDevice device = ensureVulkanDevice();
if ( device != VK_NULL_HANDLE )
{
std::lock_guard<std::mutex> lock( VulkanInitMutex() );
vkGetPhysicalDeviceProperties( device, &snapshot.vulkanProps );
vkGetPhysicalDeviceMemoryProperties( device, &snapshot.memProps );
}
}
// MNN executor capability collection (D-11)
snapshot.executorCaps = CollectExecutorCapabilities( passFactories, mnnProcessorCount );
// Disk space query (D-16)
snapshot.availableDiskBytes = QueryAvailableDiskBytes( "." );
// Executor identity hash (D-08)
{
std::vector<uint8_t> hashInput;
auto appendBytes = [&hashInput]( const void *data, size_t size )
{
const auto *bytes = static_cast<const uint8_t *>( data );
hashInput.insert( hashInput.end(), bytes, bytes + size );
};
appendBytes( &snapshot.vulkanProps.deviceID,
sizeof( snapshot.vulkanProps.deviceID ) );
appendBytes( &snapshot.vulkanProps.driverVersion,
sizeof( snapshot.vulkanProps.driverVersion ) );
appendBytes( &snapshot.vulkanProps.vendorID,
sizeof( snapshot.vulkanProps.vendorID ) );
appendBytes( snapshot.vulkanProps.deviceName,
std::strlen( snapshot.vulkanProps.deviceName ) );
for ( uint32_t i = 0; i < snapshot.memProps.memoryHeapCount; ++i )
{
appendBytes( &snapshot.memProps.memoryHeaps[i].size,
sizeof( snapshot.memProps.memoryHeaps[i].size ) );
appendBytes( &snapshot.memProps.memoryHeaps[i].flags,
sizeof( snapshot.memProps.memoryHeaps[i].flags ) );
}
for ( const auto &cap : snapshot.executorCaps )
{
auto pt = static_cast<int>( cap.passType );
appendBytes( &pt, sizeof( pt ) );
appendBytes( cap.backend.data(), cap.backend.size() );
for ( const auto &fmt : cap.supportedModelFormats )
appendBytes( fmt.data(), fmt.size() );
for ( const auto &q : cap.supportedQuantizations )
appendBytes( q.data(), q.size() );
}
snapshot.identityHash = sgns::sgprocmanagersha::sha256(
hashInput.data(), hashInput.size() );
}
m_impl->snapshot = std::move( snapshot );
m_impl->snapshotBuilt = true;
}
const CapabilitySnapshot *CapabilityValidator::GetSnapshot() const
{
if ( !m_impl->snapshotBuilt ) return nullptr;
return &m_impl->snapshot;
}
// =========================================================================
// CanExecute — all five validation categories (CAP-02..05)
// =========================================================================
void CapabilityValidator::CanExecute( const sgns::Pass &pass,
CanExecuteCallback callback )
{
CanExecuteResult result;
std::vector<UnmetRequirement> unmet;
if ( !m_impl->snapshotBuilt )
{
result.executable = false;
result.unmet.push_back(
{ UnmetRequirementCategory::RESOURCE,
"CapabilityValidator not initialized" } );
callback( result );
return;
}
const auto &snapshot = m_impl->snapshot;
PassType passType = pass.get_type();
// —— Step 1: PassType registration check (CAP-04/D-04) ——
const ExecutorCapability *executorCap = FindExecutorCap( snapshot, passType );
if ( !executorCap )
{
unmet.push_back(
{ UnmetRequirementCategory::PASS_TYPE,
"No executor registered for PassType "
+ PassTypeToString( passType ) + " ("
+ std::to_string( static_cast<int>( passType ) ) + ")"
+ ". Available: ["
+ ListAvailablePassTypes( snapshot.executorCaps ) + "]" } );
result.executable = false;
result.unmet = std::move( unmet );
callback( result );
return;
}
// —— Step 2: Vulkan feature/limit check (CAP-02/D-14) ——
if ( passType == PassType::RENDER )
{
const auto &limits = snapshot.vulkanProps.limits;
if ( snapshot.vulkanProps.deviceType != VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
&& snapshot.vulkanProps.deviceType != VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU )
{
unmet.push_back(
{ UnmetRequirementCategory::VULKAN,
"Device type not acceptable (need DISCRETE_GPU or INTEGRATED_GPU)" } );
}
if ( auto rt = pass.get_render_target() )
{
uint32_t w = static_cast<uint32_t>( rt->get_width() );
uint32_t h = static_cast<uint32_t>( rt->get_height() );
if ( w > limits.maxImageDimension2D )
unmet.push_back(
{ UnmetRequirementCategory::VULKAN,
"maxImageDimension2D: need " + std::to_string( w )
+ ", have " + std::to_string( limits.maxImageDimension2D ) } );
if ( h > limits.maxImageDimension2D )
unmet.push_back(
{ UnmetRequirementCategory::VULKAN,
"maxImageDimension2D: need " + std::to_string( h )
+ ", have " + std::to_string( limits.maxImageDimension2D ) } );
}
if ( limits.maxColorAttachments < 1 )
unmet.push_back(
{ UnmetRequirementCategory::VULKAN,
"maxColorAttachments: need 1, have "
+ std::to_string( limits.maxColorAttachments ) } );
if ( limits.maxMemoryAllocationCount < 4 )
unmet.push_back(
{ UnmetRequirementCategory::VULKAN,
"maxMemoryAllocationCount: need 4, have "
+ std::to_string( limits.maxMemoryAllocationCount ) } );
}
if ( !unmet.empty() )
{
result.executable = false;
result.unmet = std::move( unmet );
callback( result );
return;
}
// —— Step 2b: MNN model format check (CAP-03/D-11) ——
if ( passType == PassType::INFERENCE || passType == PassType::RETRAIN )
{
if ( auto model = pass.get_model() )
{
std::string fmtStr = ModelFormatToString( model->get_format() );
bool formatSupported = false;
for ( const auto &sf : executorCap->supportedModelFormats )
{
if ( sf == fmtStr ) { formatSupported = true; break; }
}
if ( !formatSupported )
unmet.push_back(
{ UnmetRequirementCategory::MNN,
"Model format " + fmtStr + " not supported. Supported: ["
+ JoinStrings( executorCap->supportedModelFormats, ", " )
+ "]" } );
}
}
if ( !unmet.empty() )
{
result.executable = false;
result.unmet = std::move( unmet );
callback( result );
return;
}
// —— Step 3: GPU memory estimation (CAP-05/D-15) ——
if ( passType == PassType::RENDER )
{
uint64_t estimatedGpuMem = 0;
if ( auto rt = pass.get_render_target() )
{
uint64_t w = static_cast<uint64_t>( rt->get_width() );
uint64_t h = static_cast<uint64_t>( rt->get_height() );
uint64_t colorBytes = BytesPerPixel( rt->get_color_format() );
uint64_t depthBytes = BytesPerPixel( rt->get_depth_format() );
estimatedGpuMem += w * h * ( colorBytes + depthBytes );
}
estimatedGpuMem += 64ULL * 1024 * 1024; // pipeline overhead
uint64_t largestHeap = 0;
for ( uint32_t i = 0; i < snapshot.memProps.memoryHeapCount; ++i )
{
if ( snapshot.memProps.memoryHeaps[i].flags
& VK_MEMORY_HEAP_DEVICE_LOCAL_BIT )
{
largestHeap = (std::max)( largestHeap,
snapshot.memProps.memoryHeaps[i].size );
}
}
if ( largestHeap > 0 && estimatedGpuMem > largestHeap )
unmet.push_back(
{ UnmetRequirementCategory::RESOURCE,
"Estimated GPU memory " + FormatBytes( estimatedGpuMem )
+ " exceeds largest device-local heap "
+ FormatBytes( largestHeap ) } );
}
// —— Step 4: Disk space check (CAP-05/D-16) ——
if ( snapshot.availableDiskBytes > 0 )
{
uint64_t estimatedOutputSize = 0;
if ( passType == PassType::RENDER )
{
if ( auto rt = pass.get_render_target() )
{
uint64_t w = static_cast<uint64_t>( rt->get_width() );
uint64_t h = static_cast<uint64_t>( rt->get_height() );
estimatedOutputSize = w * h
* BytesPerPixel( rt->get_color_format() );
}
}
if ( estimatedOutputSize > snapshot.availableDiskBytes )
unmet.push_back(
{ UnmetRequirementCategory::RESOURCE,
"Estimated output size " + FormatBytes( estimatedOutputSize )
+ " exceeds available disk space "
+ FormatBytes( snapshot.availableDiskBytes ) } );
}
// —— Build final result ——
if ( !unmet.empty() )
{
result.executable = false;
result.unmet = std::move( unmet );
}
else
{
result.executable = true;
result.executorId = DeriveExecutorId( snapshot.identityHash );
}
callback( result );
}
#ifdef SGPROCMGR_TEST_FRIEND
void CapabilityValidator::SetSnapshotForTest( CapabilitySnapshot snap )
{
m_impl->snapshot = std::move( snap );
m_impl->snapshotBuilt = true;
}
#endif
} // namespace sgns::sgprocessing
Updated on 2026-09-17 at 06:29:15 +0000