[QNN EP] Add provider option to offload graph I/O quantization/dequantization to the CPU EP (#22436)

### Description
Adds QNN provider option `offload_graph_io_quantization` to offload
graph input quantization and graph output dequantization to the CPU EP.
Option is disabled by default to maintain current behavior.


### Motivation and Context
Offloading the handling of I/O quantization to the CPU EP significantly
improves inference latency for many models.
This commit is contained in:
Adrian Lizarraga 2024-10-16 15:00:53 -07:00 committed by GitHub
parent b7050c8390
commit 84d48b6ad6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 172 additions and 24 deletions

View file

@ -3651,13 +3651,17 @@ struct OrtApi {
* - "73"
* - "75"
* "device_id": The ID of the device to use when setting 'htp_arch'. Defaults to "0" (for single device).
"enable_htp_fp16_precision": Used for float32 model for HTP backend.
Enable the float32 model to be inferenced with fp16 precision. Otherwise, it will be fp32 precision.
- "0": With fp32 precision.
- "1": Default. With fp16 precision.
"enable_htp_weight_sharing": Enable QNN weight sharing feature while compiling multiple graphs into one QNN context.
- "0": Default. Disabled.
- "1": Enabled.
* "enable_htp_fp16_precision": Used for float32 model for HTP backend.
* Enable the float32 model to be inferenced with fp16 precision. Otherwise, it will be fp32 precision.
* - "0": With fp32 precision.
* - "1": Default. With fp16 precision.
* "enable_htp_weight_sharing": Enable QNN weight sharing feature while compiling multiple graphs into one QNN context.
* - "0": Default. Disabled.
* - "1": Enabled.
* "offload_graph_io_quantization": Offload graph input quantization and graph output dequantization to another
* execution provider (typically CPU EP).
* - "0": Default. Disabled. QNN EP will handle quantization and dequantization of graph I/O.
* - "1": Enabled.
*
* SNPE supported keys:
* "runtime": SNPE runtime engine, options: "CPU", "CPU_FLOAT32", "GPU", "GPU_FLOAT32_16_HYBRID", "GPU_FLOAT16",

View file

@ -164,6 +164,11 @@ Status SimpleOpBuilder::ExplicitOpCheck(QnnModelWrapper& qnn_model_wrapper,
int64_t quant_axis = 0;
ORT_RETURN_IF_ERROR(qnn_model_wrapper.IsPerChannelQuantized(node_unit.Inputs()[0], is_per_chan_quant, quant_axis));
ORT_RETURN_IF(is_per_chan_quant, "QNN EP does not support a standalone DQ op with per-channel quantization");
if (qnn_model_wrapper.GetModelSettings().offload_graph_io_quantization) {
ORT_RETURN_IF(qnn_model_wrapper.IsGraphOutput(node_unit.Outputs()[0].node_arg.Name()),
"QNN EP is configured to not take DQ nodes that generate a graph output.");
}
}
if (op_type == "QuantizeLinear") {
@ -171,6 +176,11 @@ Status SimpleOpBuilder::ExplicitOpCheck(QnnModelWrapper& qnn_model_wrapper,
int64_t quant_axis = 0;
ORT_RETURN_IF_ERROR(qnn_model_wrapper.IsPerChannelQuantized(node_unit.Outputs()[0], is_per_chan_quant, quant_axis));
ORT_RETURN_IF(is_per_chan_quant, "QNN EP does not support a standalone Q op with per-channel quantization");
if (qnn_model_wrapper.GetModelSettings().offload_graph_io_quantization) {
ORT_RETURN_IF(qnn_model_wrapper.IsGraphInput(node_unit.Inputs()[0].node_arg.Name()),
"QNN EP is configured to not take Q nodes that consume a graph input.");
}
}
return Status::OK();

View file

@ -95,6 +95,7 @@ const NodeUnit& QnnModel::GetNodeUnit(const Node* node,
Status QnnModel::ComposeGraph(const GraphViewer& graph_viewer,
const onnxruntime::Node& fused_node,
const qnn::ModelSettings& model_settings,
const logging::Logger& logger,
const QnnGraph_Config_t** graph_configs) {
LOGS(logger, VERBOSE) << "ComposeGraph Graph name: " << graph_viewer.Name();
@ -115,7 +116,8 @@ Status QnnModel::ComposeGraph(const GraphViewer& graph_viewer,
model_input_index_map_,
model_output_index_map_,
initializer_inputs_,
qnn_backend_manager_->GetQnnBackendType());
qnn_backend_manager_->GetQnnBackendType(),
model_settings);
bool rt = true;
rt = qnn_model_wrapper.CreateQnnGraph(qnn_backend_manager_->GetQnnContext(), graph_name, graph_configs);
if (!rt) {

View file

@ -35,6 +35,7 @@ class QnnModel {
Status ComposeGraph(const GraphViewer& graph_viewer,
const onnxruntime::Node& fused_node,
const qnn::ModelSettings& model_settings,
const logging::Logger& logger,
const QnnGraph_Config_t** graph_configs = nullptr);

View file

@ -29,6 +29,10 @@ struct TensorInfo {
const ONNX_NAMESPACE::TensorProto* initializer_tensor;
};
struct ModelSettings {
bool offload_graph_io_quantization = false;
};
class QnnModelWrapper {
public:
QnnModelWrapper(const GraphViewer& graph_viewer,
@ -38,7 +42,8 @@ class QnnModelWrapper {
const std::unordered_map<std::string, size_t>& input_index_map,
const std::unordered_map<std::string, size_t>& output_index_map,
const std::unordered_set<std::string>& initializer_lookup,
QnnBackendType qnn_backend_type)
QnnBackendType qnn_backend_type,
const ModelSettings& model_settings)
: graph_viewer_(graph_viewer),
logger_(logger),
qnn_interface_(qnn_interface),
@ -46,12 +51,15 @@ class QnnModelWrapper {
input_index_map_(input_index_map),
output_index_map_(output_index_map),
initializer_lookup_(initializer_lookup),
qnn_backend_type_(qnn_backend_type) {
qnn_backend_type_(qnn_backend_type),
model_settings_(model_settings) {
}
ORT_DISALLOW_COPY_ASSIGNMENT_AND_MOVE(QnnModelWrapper);
~QnnModelWrapper() = default;
const ModelSettings& GetModelSettings() const { return model_settings_; }
bool CreateQnnGraph(const Qnn_ContextHandle_t& context,
const std::string& graph_name,
const QnnGraph_Config_t** graph_configs = nullptr);
@ -279,6 +287,7 @@ class QnnModelWrapper {
const std::unordered_map<std::string, size_t>& output_index_map_;
const std::unordered_set<std::string>& initializer_lookup_;
QnnBackendType qnn_backend_type_ = QnnBackendType::CPU;
ModelSettings model_settings_ = {};
}; // QnnModelWrapper
} // namespace qnn

View file

@ -161,6 +161,23 @@ static void ParseHtpArchitecture(const std::string& htp_arch_string, QnnHtpDevic
}
}
static bool ParseBoolOption(const std::string& key, bool default_value,
const std::unordered_map<std::string, std::string>& options) {
bool result = default_value;
auto it = options.find(key);
if (it != options.end()) {
if ("1" == it->second) {
result = true;
} else if ("0" == it->second) {
result = false;
} else {
LOGS_DEFAULT(VERBOSE) << "Invalid value for " << key << " (" << it->second << "). Only 0 or 1 allowed.";
}
LOGS_DEFAULT(VERBOSE) << "Using " << key << ": " << result;
}
return result;
}
qnn::ProfilingLevel QNNExecutionProvider::GetProfilingLevelFromETWLevel(unsigned char level) {
if (level == 5) {
LOGS_DEFAULT(INFO) << "Overriding profiling to basic based on ETW level: " << static_cast<int>(level);
@ -403,6 +420,15 @@ QNNExecutionProvider::QNNExecutionProvider(const ProviderOptions& provider_optio
LOGS_DEFAULT(VERBOSE) << "User specified enable_htp_weight_sharing: " << enable_htp_weight_sharing_;
}
model_settings_.offload_graph_io_quantization = ParseBoolOption("offload_graph_io_quantization", false,
provider_options_map);
if (disable_cpu_ep_fallback_ && model_settings_.offload_graph_io_quantization) {
LOGS_DEFAULT(WARNING) << "Fallback to CPU EP is disabled, but user configured QNN EP to offload graph I/O "
<< "quantization/dequantization to another EP. Session creation will fail if the CPU EP "
<< "handles the graph I/O quantization/dequantization.";
}
qnn_backend_manager_ = std::make_unique<qnn::QnnBackendManager>(
std::move(backend_path),
profiling_level_etw,
@ -499,7 +525,8 @@ QNNExecutionProvider::GetSupportedNodes(const GraphViewer& graph_viewer,
model_input_index_map,
model_output_index_map,
initializer_input_lookup,
qnn_backend_manager_->GetQnnBackendType());
qnn_backend_manager_->GetQnnBackendType(),
model_settings_);
std::vector<std::unique_ptr<qnn::IQnnNodeGroup>> qnn_node_groups;
qnn_node_groups.reserve(node_unit_size);
@ -845,7 +872,8 @@ Status QNNExecutionProvider::CompileFromOrtGraph(const std::vector<FusedNodeAndG
QNN_HTP_GRAPH_CUSTOM_CONFIG_INIT);
InitQnnGraphConfigs(graph_configs_builder);
ORT_RETURN_IF_ERROR(qnn_model->ComposeGraph(graph_viewer, fused_node, logger, graph_configs_builder.GetQnnConfigs()));
ORT_RETURN_IF_ERROR(qnn_model->ComposeGraph(graph_viewer, fused_node, model_settings_, logger,
graph_configs_builder.GetQnnConfigs()));
ORT_RETURN_IF_ERROR(qnn_model->FinalizeGraphs(logger));
ORT_RETURN_IF_ERROR(qnn_model->SetupQnnInputOutput(logger));

View file

@ -153,6 +153,7 @@ class QNNExecutionProvider : public IExecutionProvider {
#ifdef _WIN32
onnxruntime::logging::EtwRegistrationManager::EtwInternalCallback callback_ETWSink_provider_;
#endif
qnn::ModelSettings model_settings_ = {};
class PerThreadContext final {
public:

View file

@ -77,6 +77,8 @@ void usage() {
"\t [QNN only] [device_id]: The ID of the device to use when setting 'htp_arch'. Defaults to '0' (for single device). \n"
"\t [QNN only] [enable_htp_fp16_precision]: Enable the HTP_FP16 precision so that the float32 model will be inferenced with fp16 precision. \n"
"\t Otherwise, it will be fp32 precision. Works for float32 model for HTP backend. Defaults to '1' (with FP16 precision.). \n"
"\t [QNN only] [offload_graph_io_quantization]: Offload graph input quantization and graph output dequantization to another EP (typically CPU EP). \n"
"\t Defaults to '0' (QNN EP handles the graph I/O quantization and dequantization). \n"
"\t [Usage]: -e <provider_name> -i '<key1>|<value1> <key2>|<value2>' \n\n"
"\t [Example] [For QNN EP] -e qnn -i \"profiling_level|detailed backend_path|/folderpath/libQnnCpu.so\" \n\n"
"\t [SNPE only] [runtime]: SNPE runtime, options: 'CPU', 'GPU', 'GPU_FLOAT16', 'DSP', 'AIP_FIXED_TF'. \n"
@ -587,20 +589,20 @@ int real_main(int argc, char* argv[], Ort::Env& env) {
std::string str = str_stream.str();
ORT_THROW("Wrong value for htp_arch. select from: " + str);
}
} else if (key == "enable_htp_fp16_precision") {
} else if (key == "enable_htp_fp16_precision" || key == "offload_graph_io_quantization") {
std::unordered_set<std::string> supported_options = {"0", "1"};
if (supported_options.find(value) == supported_options.end()) {
std::ostringstream str_stream;
std::copy(supported_options.begin(), supported_options.end(),
std::ostream_iterator<std::string>(str_stream, ","));
std::string str = str_stream.str();
ORT_THROW("Wrong value for enable_htp_fp16_precision. select from: " + str);
ORT_THROW("Wrong value for ", key, ". select from: ", str);
}
} else {
ORT_THROW(R"(Wrong key type entered. Choose from options: ['backend_path',
'profiling_level', 'profiling_file_path', 'rpc_control_latency', 'vtcm_mb', 'htp_performance_mode',
'qnn_saver_path', 'htp_graph_finalization_optimization_mode', 'qnn_context_priority',
'soc_model', 'htp_arch', 'device_id', 'enable_htp_fp16_precision'])");
'soc_model', 'htp_arch', 'device_id', 'enable_htp_fp16_precision', 'offload_graph_io_quantization'])");
}
qnn_options[key] = value;

View file

@ -98,6 +98,8 @@ namespace perftest {
"\t [QNN only] [device_id]: The ID of the device to use when setting 'htp_arch'. Defaults to '0' (for single device). \n"
"\t [QNN only] [enable_htp_fp16_precision]: Enable the HTP_FP16 precision so that the float32 model will be inferenced with fp16 precision. \n"
"\t Otherwise, it will be fp32 precision. Works for float32 model for HTP backend. Defaults to '1' (with FP16 precision.). \n"
"\t [QNN only] [offload_graph_io_quantization]: Offload graph input quantization and graph output dequantization to another EP (typically CPU EP). \n"
"\t Defaults to '0' (QNN EP handles the graph I/O quantization and dequantization). \n"
"\t [Example] [For QNN EP] -e qnn -i \"backend_path|/folderpath/libQnnCpu.so\" \n"
"\n"
"\t [TensorRT only] [trt_max_partition_iterations]: Maximum iterations for TensorRT parser to get capability.\n"

View file

@ -302,20 +302,20 @@ OnnxRuntimeTestSession::OnnxRuntimeTestSession(Ort::Env& env, std::random_device
std::string str = str_stream.str();
ORT_THROW("Wrong value for htp_arch. select from: " + str);
}
} else if (key == "enable_htp_fp16_precision") {
} else if (key == "enable_htp_fp16_precision" || key == "offload_graph_io_quantization") {
std::unordered_set<std::string> supported_options = {"0", "1"};
if (supported_options.find(value) == supported_options.end()) {
std::ostringstream str_stream;
std::copy(supported_options.begin(), supported_options.end(),
std::ostream_iterator<std::string>(str_stream, ","));
std::string str = str_stream.str();
ORT_THROW("Wrong value for " + key + ". select from: " + str);
ORT_THROW("Wrong value for ", key, ". select from: ", str);
}
} else {
ORT_THROW(R"(Wrong key type entered. Choose from options: ['backend_path',
'profiling_level', 'profiling_file_path', 'rpc_control_latency', 'vtcm_mb', 'htp_performance_mode',
'qnn_saver_path', 'htp_graph_finalization_optimization_mode', 'qnn_context_priority', 'soc_model',
'htp_arch', 'device_id', 'enable_htp_fp16_precision'])");
'htp_arch', 'device_id', 'enable_htp_fp16_precision', 'offload_graph_io_quantization'])");
}
qnn_options[key] = value;

View file

@ -1023,6 +1023,81 @@ TEST_F(QnnHTPBackendTests, EPRejectsDynamicShapesF32) {
&ep_graph_checker);
}
// Test option for offloading quantization of graph inputs and dequantization of graph outputs to the CPU EP.
TEST_F(QnnHTPBackendTests, EPOffloadsGraphIOQuantDequant) {
// Returns a function that checks that the Q/DQ ops at the graph IO boundary are offloaded to CPU
// if the corresponding provider option is enabled.
auto graph_checker_builder = [](bool offload_graph_io_quantization) -> std::function<void(const Graph&)> {
return [offload_graph_io_quantization](const Graph& graph) {
size_t num_q = 0;
size_t num_dq = 0;
size_t num_qnn_fused_node = 0;
for (const Node& node : graph.Nodes()) {
const std::string& ep_name = node.GetExecutionProviderType();
const std::string& op_type = node.OpType();
if (offload_graph_io_quantization && op_type == "QuantizeLinear") {
const bool consumes_graph_input = graph.IsInputsIncludingInitializers(node.InputDefs()[0]);
EXPECT_EQ(ep_name, kCpuExecutionProvider);
EXPECT_TRUE(consumes_graph_input);
num_q += 1;
} else if (offload_graph_io_quantization && op_type == "DequantizeLinear") {
const bool produces_graph_output = graph.IsOutput(node.OutputDefs()[0]);
EXPECT_EQ(ep_name, kCpuExecutionProvider);
EXPECT_TRUE(produces_graph_output);
num_dq += 1;
} else {
EXPECT_EQ(ep_name, kQnnExecutionProvider);
num_qnn_fused_node += 1;
}
}
EXPECT_EQ(num_q, static_cast<size_t>(offload_graph_io_quantization));
EXPECT_EQ(num_dq, static_cast<size_t>(offload_graph_io_quantization));
EXPECT_EQ(num_qnn_fused_node, 1);
};
};
ProviderOptions provider_options;
#if defined(_WIN32)
provider_options["backend_path"] = "QnnHtp.dll";
#else
provider_options["backend_path"] = "libQnnHtp.so";
#endif
const std::vector<std::string> op_types = {
"Sigmoid",
"Transpose",
"Softmax",
"Sqrt",
"Elu",
};
// Test various QDQ ops with offloading of I/O quantization enabled and disabled.
for (auto op_type : op_types) {
for (int offload_io_quant = 0; offload_io_quant <= 1; offload_io_quant++) {
provider_options["offload_graph_io_quantization"] = offload_io_quant ? "1" : "0";
auto graph_checker = graph_checker_builder(offload_io_quant);
auto expected_ep_assignment = offload_io_quant ? ExpectedEPNodeAssignment::Some : ExpectedEPNodeAssignment::All;
float min_val = (op_type == "Sqrt") ? 0.0f : -10.0f;
TestInputDef<float> input_def({1, 2, 2, 2}, false, GetFloatDataInRange(min_val, 10.0f, 8));
auto f32_model_build_fn = BuildOpTestCase<float>(op_type, {input_def}, {}, {});
auto qdq_model_build_fn = BuildQDQOpTestCase<uint8_t>(op_type, {input_def}, {}, {});
TestQDQModelAccuracy<uint8_t>(f32_model_build_fn,
qdq_model_build_fn,
provider_options,
/*opset*/ 21,
expected_ep_assignment,
/*abs_err*/ QDQTolerance(),
logging::Severity::kERROR,
/*qnn_ctx_model_path*/ "",
/*session_option_pairs*/ {},
&graph_checker);
}
}
}
#endif // defined(__aarch64__) || defined(_M_ARM64) || defined(__linux__)
#endif // !defined(ORT_MINIMAL_BUILD)

View file

@ -134,7 +134,8 @@ void InferenceModel(const std::string& model_data, const char* log_id,
ExpectedEPNodeAssignment expected_ep_assignment, const NameMLValMap& feeds,
std::vector<OrtValue>& output_vals,
bool is_qnn_ep,
const std::unordered_map<std::string, std::string>& session_option_pairs) {
const std::unordered_map<std::string, std::string>& session_option_pairs,
std::function<void(const Graph&)>* graph_checker) {
SessionOptions so;
so.session_logid = log_id;
for (auto key_value : session_option_pairs) {
@ -166,6 +167,10 @@ void InferenceModel(const std::string& model_data, const char* log_id,
ASSERT_GT(ep_nodes, 0) << "No nodes were assigned to " << provider_type;
}
if (graph_checker) {
(*graph_checker)(graph);
}
const auto& outputs = graph.GetOutputs();
std::vector<std::string> output_names;

View file

@ -457,13 +457,15 @@ DEF_QUANTIZE_VALUES_INT4_FUNC(UInt4x2, ParQuantizeLinearStdU4)
* \param output_vals Initialized to the inference results.
* \param is_qnn_ep Ture: QNN EP is used. False: CPU EP is used (default).
* \param session_option_pairs extra session options.
* \param graph_checker Function called on the Graph.
*/
void InferenceModel(const std::string& model_data, const char* log_id,
const ProviderOptions& provider_options,
ExpectedEPNodeAssignment expected_ep_assignment, const NameMLValMap& feeds,
std::vector<OrtValue>& output_vals,
bool is_qnn_ep = false,
const std::unordered_map<std::string, std::string>& session_option_pairs = {});
const std::unordered_map<std::string, std::string>& session_option_pairs = {},
std::function<void(const Graph&)>* graph_checker = nullptr);
/**
* If the ORT_UNIT_TEST_ENABLE_QNN_SAVER environment variable is enabled (set to 1), this function modifies
@ -515,6 +517,8 @@ struct QDQTolerance {
* \param tolerance The percent tolerance (as fraction) QNN EP results are allowed to differ from the QDQ model
* on CPU EP. This tolerance is a percentage of the output range.
* \param log_severity The logger's severity setting.
* \param ep_graph_checker Function called on the Graph generated for the QNN EP's session. Used to check node
* EP assignment.
*/
template <typename QuantType>
inline void TestQDQModelAccuracy(const GetTestModelFn& f32_model_fn, const GetTestQDQModelFn<QuantType>& qdq_model_fn,
@ -523,7 +527,8 @@ inline void TestQDQModelAccuracy(const GetTestModelFn& f32_model_fn, const GetTe
QDQTolerance tolerance = QDQTolerance(),
logging::Severity log_severity = logging::Severity::kERROR,
const std::string& qnn_ctx_model_path = "",
const std::unordered_map<std::string, std::string>& session_option_pairs = {}) {
const std::unordered_map<std::string, std::string>& session_option_pairs = {},
std::function<void(const Graph&)>* qnn_ep_graph_checker = nullptr) {
// Add kMSDomain to cover contrib op like Gelu
const std::unordered_map<std::string, int> domain_to_version = {{"", opset_version}, {kMSDomain, 1}};
@ -607,7 +612,7 @@ inline void TestQDQModelAccuracy(const GetTestModelFn& f32_model_fn, const GetTe
// Run QDQ model on QNN EP and collect outputs.
// Only need to apply the extra session options to this QDQ model inference on QNN EP
InferenceModel(qdq_model_data, "qdq_model_logger", qnn_options, expected_ep_assignment,
qdq_helper.feeds_, qnn_qdq_outputs, is_qnn_ep, session_option_pairs);
qdq_helper.feeds_, qnn_qdq_outputs, is_qnn_ep, session_option_pairs, qnn_ep_graph_checker);
}
if (expected_ep_assignment != ExpectedEPNodeAssignment::None) {

View file

@ -48,6 +48,8 @@ namespace qnnctxgen {
"\t [enable_htp_fp16_precision]: Enable the HTP_FP16 precision so that the float32 model will be inferenced with fp16 precision. \n"
"\t Otherwise, it will be fp32 precision. Works for float32 model for HTP backend. Defaults to '1' (with FP16 precision.). \n"
"\t [enable_htp_weight_sharing]: Allows common weights across graphs to be shared and stored in a single context binary. Defaults to '1' (enabled).\n"
"\t [offload_graph_io_quantization]: Offload graph input quantization and graph output dequantization to another EP (typically CPU EP). \n"
"\t Defaults to '0' (QNN EP handles the graph I/O quantization and dequantization). \n"
"\t [Example] -i \"vtcm_mb|8 htp_arch|73\" \n"
"\n"
"\t-h: help\n");
@ -143,7 +145,8 @@ static bool ParseSessionConfigs(const std::string& configs_string,
std::string str = str_stream.str();
ORT_THROW("Wrong value for htp_graph_finalization_optimization_mode. select from: " + str);
}
} else if (key == "enable_htp_fp16_precision" || key == "enable_htp_weight_sharing") {
} else if (key == "enable_htp_fp16_precision" || key == "enable_htp_weight_sharing" ||
key == "offload_graph_io_quantization") {
std::unordered_set<std::string> supported_options = {"0", "1"};
if (supported_options.find(value) == supported_options.end()) {
std::ostringstream str_stream;
@ -154,7 +157,8 @@ static bool ParseSessionConfigs(const std::string& configs_string,
}
} else {
ORT_THROW(R"(Wrong key type entered. Choose from options: ['backend_path', 'vtcm_mb', 'htp_performance_mode',
'htp_graph_finalization_optimization_mode', 'soc_model', 'htp_arch', 'enable_htp_fp16_precision', 'enable_htp_weight_sharing'])");
'htp_graph_finalization_optimization_mode', 'soc_model', 'htp_arch', 'enable_htp_fp16_precision', 'enable_htp_weight_sharing',
'offload_graph_io_quantization'])");
}
test_config.run_config.qnn_options[key] = value;