processors/processing_processor_mnn_string.cpp¶
Namespaces¶
| Name |
|---|
| sgns |
| sgns::sgprocessing Artifact and manifest binary serialization. |
Source code¶
#include "processors/processing_processor_mnn_string.hpp"
#include "processors/vulkan_gpu_probe.hpp"
#include "processingbase/vulkan_init_guard.hpp"
#include <functional>
#include <mutex>
#include <thread>
#include <sstream>
#include <algorithm>
#include <cstring>
#include <openssl/sha.h> // For SHA256_DIGEST_LENGTH
#include "util/sha256.hpp"
#include "util/quantization.hpp"
namespace sgns::sgprocessing
{
using namespace MNN;
namespace
{
bool TryParseTokenIds( const std::string &text, std::vector<int32_t> &tokenIds )
{
std::istringstream stream( text );
int64_t value = 0;
while ( stream >> value )
{
tokenIds.push_back( static_cast<int32_t>( value ) );
}
if ( tokenIds.empty() )
{
return false;
}
stream.clear();
stream >> std::ws;
return stream.eof();
}
std::string ToLowerAscii( std::string value )
{
std::transform( value.begin(), value.end(), value.begin(),
[]( unsigned char c ) { return static_cast<char>( std::tolower( c ) ); } );
return value;
}
}
ProcessingResult MNN_String::StartProcessing( std::vector<std::vector<uint8_t>> &chunkhashes,
const sgns::IoDeclaration &proc,
std::vector<char> &textData,
std::vector<char> &modelFile,
const std::vector<sgns::Parameter> *parameters,
const ExecutionContext &execCtx )
{
std::vector<uint8_t> modelFile_bytes;
modelFile_bytes.assign(modelFile.begin(), modelFile.end());
std::vector<uint8_t> subTaskResultHash(SHA256_DIGEST_LENGTH);
// Convert text data to string
std::string inputText( textData.begin(), textData.end() );
m_logger->info( "Processing text input: {}", inputText );
// For string inputs, we process as a single "chunk"
m_progress = 0.0f;
std::vector<uint8_t> shahash( SHA256_DIGEST_LENGTH );
// Default max length, overridden below by the job's schema-declared "maxLength"
// parameter when present. Different models compiled into different jobs require
// different FIXED input sequence lengths (e.g. 128 for the legacy multi-input BERT
// model, 16 for the tiny single-input embedding model) -- a single hardcoded literal
// cannot serve both, since resizing a fixed-shape model's input to any length other
// than the one baked in at export time breaks its downstream fully-connected layer.
const float scale = sgprocmanagerquant::ResolveQuantScale( parameters );
int maxLength = 128;
if ( parameters )
{
for ( const auto ¶m : *parameters )
{
if ( param.get_name() == "maxLength" && param.get_type() == sgns::ParameterType::INT )
{
const auto &def = param.get_parameter_default();
if ( def.is_number_integer() && def.get<int>() > 0 )
{
maxLength = def.get<int>();
}
break;
}
}
}
std::vector<int32_t> tokenIds;
bool parsedTokenIds = TryParseTokenIds( inputText, tokenIds );
if ( parsedTokenIds )
{
if ( static_cast<int>( tokenIds.size() ) > maxLength )
{
tokenIds.resize( maxLength );
}
m_logger->info( "Parsed {} token id(s) from input", tokenIds.size() );
}
else
{
m_logger->info( "Input is not token ids; using character codes as fallback" );
tokenIds.reserve( std::min( static_cast<int>( inputText.size() ), maxLength ) );
for ( size_t i = 0; i < inputText.size() && static_cast<int>( i ) < maxLength; ++i )
{
tokenIds.push_back( static_cast<int32_t>( static_cast<unsigned char>( inputText[i] ) ) );
}
}
// resizeLen is the model's fixed/declared sequence length (from the schema's
// maxLength parameter, clamped to at least 1) -- NOT the raw token count -- so a
// fixed-shape model's input tensor is always resized to the exact length its
// compiled graph expects, regardless of how many tokens were actually parsed
// (shorter inputs are zero-padded by the existing fill loop in Process()).
const int resizeLen = std::max( 1, maxLength );
auto procresults = Process( tokenIds, modelFile_bytes, resizeLen );
if ( !procresults || procresults->elementSize() == 0 )
{
m_logger->error( "MNN string processing produced no output (see prior errors)" );
ProcessingResult errResult;
errResult.error = ProcessingError{ ProcessingErrorStage::UNSPECIFIED,
"MNN string processing produced no output" };
return errResult;
}
const float *data = procresults->host<float>();
size_t dataSize = procresults->elementSize() * sizeof( float );
{
std::ostringstream sample;
size_t sampleCount = std::min<size_t>( 16, procresults->elementSize() );
sample << "Output sample (first " << sampleCount << "): ";
for ( size_t i = 0; i < sampleCount; ++i )
{
if ( i > 0 )
{
sample << ", ";
}
sample << data[i];
}
m_logger->info( "{}", sample.str() );
}
// Phase 10 CAPT-02: quantize-then-capture-then-hash at the per-chunk site.
// Never mutate MNN-owned `data` (const float*) in place -- copy first.
std::vector<float> localCopy( data, data + ( dataSize / sizeof( float ) ) );
sgprocmanagerquant::QuantizeFloatBuffer( localCopy.data(), localCopy.size(), scale );
if ( execCtx.rawOutputCapture )
{
const auto *quantizedBytes = reinterpret_cast<const uint8_t *>( localCopy.data() );
const auto *preQuantizeBytes = reinterpret_cast<const uint8_t *>( data );
execCtx.rawOutputCapture( std::vector<uint8_t>( quantizedBytes, quantizedBytes + dataSize ),
std::vector<uint8_t>( preQuantizeBytes, preQuantizeBytes + dataSize ) );
}
shahash = sgprocmanagersha::sha256( localCopy.data(), dataSize );
std::string hashString( shahash.begin(), shahash.end() );
chunkhashes.push_back( shahash );
std::string combinedHash = std::string(subTaskResultHash.begin(), subTaskResultHash.end()) + hashString;
subTaskResultHash = sgprocmanagersha::sha256( combinedHash.c_str(), combinedHash.length() );
m_progress = 100.0f;
m_logger->info( "String processing complete" );
ProcessingResult result;
result.hash = subTaskResultHash;
if ( procresults && procresults->elementSize() > 0 )
{
const size_t byteCount = procresults->elementSize() * sizeof( float );
std::vector<char> outputBytes( byteCount );
std::memcpy( outputBytes.data(), data, byteCount );
result.output_buffers = std::make_shared<std::pair<std::vector<std::string>, std::vector<std::vector<char>>>>();
result.output_buffers->first.push_back( "" );
result.output_buffers->second.push_back( std::move( outputBytes ) );
}
return result;
}
std::unique_ptr<MNN::Tensor> MNN_String::Process( const std::vector<int32_t> &tokenIds,
std::vector<uint8_t> &modelFile,
const int maxLength )
{
m_logger->info( "Creating MNN interpreter from model file" );
// Create MNN interpreter from model bytes
auto interpreter = std::shared_ptr<MNN::Interpreter>(
MNN::Interpreter::createFromBuffer(modelFile.data(), modelFile.size())
);
if (!interpreter) {
m_logger->error( "Failed to create MNN interpreter" );
return std::make_unique<MNN::Tensor>();
}
// Configure session
// Precision_High keeps the Vulkan backend on FP32 tensor storage and
// FP32 shader variants: VulkanBackend.cpp silently enables FP16
// storage on FP16-capable GPUs whenever precision != Precision_High,
// which breaks absolute cross-device tolerances and masks SECV-01
// corrupted-model tamper detection.
MNN::BackendConfig backendConfig;
backendConfig.precision = MNN::BackendConfig::Precision_High;
MNN::ScheduleConfig config;
// GPU-less hosts (software Vulkan / llvmpipe only) run MNN on the CPU
// backend -- the native CPU path is far faster than Vulkan-on-lavapipe
// and matches the pre-WHOLEARCHIVE behavior those hosts always had.
config.type = HasUsableVulkanDeviceCached() ? MNN_FORWARD_VULKAN : MNN_FORWARD_CPU; // Use Vulkan backend when a real GPU exists
config.numThread = 4;
config.backendConfig = &backendConfig;
MNN::Session *session = nullptr;
{
std::lock_guard<std::mutex> lock( sgns::sgprocessing::VulkanInitMutex() );
session = interpreter->createSession(config);
}
if (!session) {
m_logger->error( "Failed to create MNN session" );
return std::make_unique<MNN::Tensor>();
}
// Get all input tensors - BERT models typically have multiple inputs
auto inputTensors = interpreter->getSessionInputAll(session);
m_logger->info( "Model has {} input tensor(s)", inputTensors.size() );
// Log all input tensor names and shapes
for (const auto& inputPair : inputTensors) {
m_logger->info( "Input '{}': shape {}x{}x{}x{}",
inputPair.first,
inputPair.second->batch(),
inputPair.second->channel(),
inputPair.second->height(),
inputPair.second->width() );
}
// Get the first input tensor (usually input_ids for BERT)
auto inputTensor = interpreter->getSessionInput(session, nullptr);
if (!inputTensor) {
m_logger->error( "Failed to get input tensor" );
return std::make_unique<MNN::Tensor>();
}
// Resize all input tensors to [batch=1, sequence_length=maxLength]
// BERT models expect: input_ids, attention_mask, token_type_ids (all same shape)
for (const auto& inputPair : inputTensors) {
auto tensor = inputPair.second;
if (tensor->elementSize() != maxLength) {
m_logger->info( "Resizing '{}' to [1, {}]", inputPair.first, maxLength );
interpreter->resizeTensor( tensor, { 1, maxLength } );
}
}
interpreter->resizeSession( session );
// Log shapes after resize
for (const auto& inputPair : inputTensors) {
m_logger->info( "After resize '{}': shape {}x{}x{}x{}",
inputPair.first,
inputPair.second->batch(),
inputPair.second->channel(),
inputPair.second->height(),
inputPair.second->width() );
}
// Fill input tensors with dummy data
// In production, you would use a proper BERT tokenizer
for (const auto& inputPair : inputTensors) {
auto tensor = inputPair.second;
MNN::Tensor inputTensorUser(tensor, tensor->getDimensionType());
auto inputData = inputTensorUser.host<int32_t>();
for (int i = 0; i < inputTensorUser.elementSize(); ++i) {
std::string inputName = ToLowerAscii( inputPair.first );
if ( inputName.find( "input_ids" ) != std::string::npos )
{
inputData[i] = ( i < static_cast<int>( tokenIds.size() ) ) ? tokenIds[i] : 0;
}
else if ( inputName.find( "attention_mask" ) != std::string::npos )
{
inputData[i] = ( i < static_cast<int>( tokenIds.size() ) ) ? 1 : 0;
}
else if ( inputName.find( "token_type_ids" ) != std::string::npos )
{
inputData[i] = 0;
}
else
{
inputData[i] = ( i < static_cast<int>( tokenIds.size() ) ) ? tokenIds[i] : 0;
}
}
tensor->copyFromHostTensor(&inputTensorUser);
m_logger->info( "Filled '{}' with {} elements", inputPair.first, inputTensorUser.elementSize() );
}
// Run inference
m_logger->info( "Running MNN inference" );
MNN::ErrorCode runResult = interpreter->runSession( session );
if ( runResult != MNN::NO_ERROR )
{
m_logger->error( "MNN runSession failed with ErrorCode {}", static_cast<int>( runResult ) );
return std::make_unique<MNN::Tensor>();
}
// Get output tensor
auto outputTensor = interpreter->getSessionOutput(session, nullptr);
if (!outputTensor) {
m_logger->error( "Failed to get output tensor" );
return std::make_unique<MNN::Tensor>();
}
m_logger->info( "Output tensor shape: {}x{}x{}x{}",
outputTensor->batch(),
outputTensor->channel(),
outputTensor->height(),
outputTensor->width() );
// Create host tensor for output
auto outputHost = std::make_unique<MNN::Tensor>(outputTensor, outputTensor->getDimensionType());
outputTensor->copyToHostTensor(outputHost.get());
m_logger->info( "MNN inference complete" );
return outputHost;
}
}
Updated on 2026-10-06 at 13:34:21 +0000