当前位置: 首页 > news >正文

TensorRT自定义量化 对数量化

以下为AI生成 仅供参考

好的,在TensorRT中实现一个自定义的对数量化层是一个非常典型的扩展TensorRT功能的任务。由于TensorRT原生不支持对数量化,我们需要通过其强大的插件(Plugin)机制来注入我们自己的实现。

下面我将分步详细解释如何实现一个对数量化插件,并提供完整的代码示例。

1. 对数量化原理

首先,我们定义对数量化的数学操作。标准的线性量化是 y = scale * x + zero_point,而对数量化则是基于对数域进行映射。

一个简单的对数量化(Logarithmic Quantization)可以定义为:

  1. 输入x (FP32 浮点数)

  2. 参数

    • base (b): 对数的底,例如 2.0
    • scale (s): 缩放因子,用于调整精度。
    • Q_min, Q_max: 量化后的数值范围,例如对于INT8,是 -128127
  3. 量化(Quantization)过程 Q(x):

    if x == 0:return 0
    sign = sgn(x)
    val = abs(x)
    quantized_val = round(log_b(val) * s)
    clipped_val = clip(quantized_val, Q_min, Q_max)
    return clipped_val * sign
    

    注意:这里的输出为了简化,我们先让它仍然是FP32类型,代表量化后的整数值。在实际应用中,输出类型可能是INT8。

  4. 反量化(Dequantization)过程 Dq(q):

    if q == 0:return 0
    sign = sgn(q)
    val = abs(q)
    dequantized_val = b^(val / s)
    return dequantized_val * sign
    

在这个示例中,我们将创建一个插件来实现上述的量化(Quantization)过程

2. TensorRT 插件 API: IPluginV2DynamicExt

为了支持动态输入尺寸(这是现代模型部署的标配),我们需要实现 IPluginV2DynamicExt 接口。它继承自 IPluginV2,并要求我们实现一系列方法来告诉TensorRT如何配置、执行和序列化我们的自定义层。

主要涉及的核心类和方法有:

  • LogQuantPlugin: 我们自定义的插件类,继承自 IPluginV2DynamicExt
  • LogQuantPluginCreator: 插件的工厂类,负责创建和反序列化插件实例。
  • enqueue(): 插件的核心执行逻辑,在这里我们将调用CUDA核函数来完成计算。
  • supportsFormatCombination(): 告诉TensorRT我们的插件支持哪些数据类型和格式组合(例如,输入是FP32,输出也是FP32)。
  • configurePlugin(): 根据实际的输入/输出配置来设置插件。
  • getOutputDimensions(): 计算并返回输出张量的维度。
  • serialize() / deserialize(): 定义如何将插件的参数(如 base, scale)保存到引擎文件以及如何从文件中恢复。
  • clone(): 克隆一个插件实例。

3. 代码实现步骤

我们将创建以下文件:

  1. log_quant_plugin.h: 插件类的头文件声明。
  2. log_quant_plugin.cu: 插件类方法的实现和CUDA核函数。
  3. CMakeLists.txt: 用于编译插件动态链接库的构建脚本。
  4. main.cpp: 一个使用该插件构建和运行TensorRT引擎的示例程序。

第1步: log_quant_plugin.h - 插件头文件
#pragma once#include "NvInfer.h"
#include "NvInferPlugin.h"
#include <string>
#include <vector>// 定义插件名称和版本
#define LOG_QUANT_PLUGIN_NAME "LogQuant"
#define LOG_QUANT_PLUGIN_VERSION "1"// 插件命名空间
namespace nvinfer1
{
namespace plugin
{class LogQuantPlugin final : public IPluginV2DynamicExt
{
public:// 构造函数,用于创建时传递参数LogQuantPlugin(float base, float scale, float quantMin, float quantMax);// 构造函数,用于反序列化LogQuantPlugin(const void* serialData, size_t serialLength);// 默认构造函数LogQuantPlugin() = delete;~LogQuantPlugin() override;// IPluginV2Ext Methodsint getNbOutputs() const noexcept override;nvinfer1::DataType getOutputDataType(int index, const nvinfer1::DataType* inputTypes, int nbInputs) const noexcept override;size_t getSerializationSize() const noexcept override;void serialize(void* buffer) const noexcept override;void destroy() noexcept override;const char* getPluginType() const noexcept override;const char* getPluginVersion() const noexcept override;void setPluginNamespace(const char* pluginNamespace) noexcept override;const char* getPluginNamespace() const noexcept override;int initialize() noexcept override;void terminate() noexcept override;// IPluginV2DynamicExt MethodsIPluginV2DynamicExt* clone() const noexcept override;DimsExprs getOutputDimensions(int outputIndex, const DimsExprs* inputs, int nbInputs, IExprBuilder& exprBuilder) noexcept override;bool supportsFormatCombination(int pos, const PluginTensorDesc* inOut, int nbInputs, int nbOutputs) noexcept override;void configurePlugin(const DynamicPluginTensorDesc* in, int nbInputs, const DynamicPluginTensorDesc* out, int nbOutputs) noexcept override;size_t getWorkspaceSize(const PluginTensorDesc* inputs, int nbInputs, const PluginTensorDesc* outputs, int nbOutputs) const noexcept override;int enqueue(const PluginTensorDesc* inputDesc, const PluginTensorDesc* outputDesc, const void* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) noexcept override;private:float mBase;float mScale;float mQuantMin;float mQuantMax;std::string mNamespace;
};class LogQuantPluginCreator : public IPluginCreator
{
public:LogQuantPluginCreator();~LogQuantPluginCreator() override = default;const char* getPluginName() const noexcept override;const char* getPluginVersion() const noexcept override;const PluginField* getFieldNames() noexcept override;IPluginV2* createPlugin(const char* name, const PluginFieldCollection* fc) noexcept override;IPluginV2* deserializePlugin(const char* name, const void* serialData, size_t serialLength) noexcept override;void setPluginNamespace(const char* pluginNamespace) noexcept override;const char* getPluginNamespace() const noexcept override;private:static PluginFieldCollection mFC;static std::vector<PluginField> mPluginAttributes;std::string mNamespace;
};} // namespace plugin
} // namespace nvinfer1

第2步: log_quant_plugin.cu - 插件实现和CUDA核函数
#include "log_quant_plugin.h"
#include <cuda_runtime.h>
#include <cmath>
#include <stdexcept>
#include <iostream>using namespace nvinfer1;
using namespace plugin;// 注册插件到TensorRT的注册表
REGISTER_TENSORRT_PLUGIN(LogQuantPluginCreator);// CUDA 核函数
__global__ void logQuantKernel(const float* input, float* output, int n, float base, float scale, float quantMin, float quantMax)
{int i = blockIdx.x * blockDim.x + threadIdx.x;if (i < n) {float val = input[i];if (val == 0.0f) {output[i] = 0.0f;} else {float sign = (val > 0.0f) ? 1.0f : -1.0f;float abs_val = fabsf(val);// 使用log2f/logf进行计算float log_val = logf(abs_val) / logf(base);float quantized_val = roundf(log_val * scale);float clipped_val = fminf(fmaxf(quantized_val, quantMin), quantMax);output[i] = clipped_val * sign;}}
}// --- LogQuantPlugin Implementation ---LogQuantPlugin::LogQuantPlugin(float base, float scale, float quantMin, float quantMax): mBase(base), mScale(scale), mQuantMin(quantMin), mQuantMax(quantMax) {}LogQuantPlugin::LogQuantPlugin(const void* serialData, size_t serialLength)
{const char* d = reinterpret_cast<const char*>(serialData);const char* a = d;mBase = *reinterpret_cast<const float*>(d); d += sizeof(float);mScale = *reinterpret_cast<const float*>(d); d += sizeof(float);mQuantMin = *reinterpret_cast<const float*>(d); d += sizeof(float);mQuantMax = *reinterpret_cast<const float*>(d); d += sizeof(float);// 断言确保我们读取的字节数是正确的if (d != a + serialLength) {throw std::runtime_error("Error in LogQuantPlugin deserialization");}
}LogQuantPlugin::~LogQuantPlugin() {}int LogQuantPlugin::getNbOutputs() const noexcept { return 1; }DataType LogQuantPlugin::getOutputDataType(int index, const DataType* inputTypes, int nbInputs) const noexcept
{// 输出类型与输入类型一致,这里我们简化为FP32return inputTypes[0];
}size_t LogQuantPlugin::getSerializationSize() const noexcept
{// 4个float参数return 4 * sizeof(float);
}void LogQuantPlugin::serialize(void* buffer) const noexcept
{char* d = reinterpret_cast<char*>(buffer);const char* a = d;*reinterpret_cast<float*>(d) = mBase; d += sizeof(float);*reinterpret_cast<float*>(d) = mScale; d += sizeof(float);*reinterpret_cast<float*>(d) = mQuantMin; d += sizeof(float);*reinterpret_cast<float*>(d) = mQuantMax; d += sizeof(float);if (d != a + getSerializationSize()) {// handle error}
}void LogQuantPlugin::destroy() noexcept { delete this; }const char* LogQuantPlugin::getPluginType() const noexcept { return LOG_QUANT_PLUGIN_NAME; }const char* LogQuantPlugin::getPluginVersion() const noexcept { return LOG_QUANT_PLUGIN_VERSION; }void LogQuantPlugin::setPluginNamespace(const char* pluginNamespace) noexcept { mNamespace = pluginNamespace; }const char* LogQuantPlugin::getPluginNamespace() const noexcept { return mNamespace.c_str(); }int LogQuantPlugin::initialize() noexcept { return 0; }void LogQuantPlugin::terminate() noexcept {}IPluginV2DynamicExt* LogQuantPlugin::clone() const noexcept
{auto plugin = new LogQuantPlugin(mBase, mScale, mQuantMin, mQuantMax);plugin->setPluginNamespace(mNamespace.c_str());return plugin;
}DimsExprs LogQuantPlugin::getOutputDimensions(int outputIndex, const DimsExprs* inputs, int nbInputs, IExprBuilder& exprBuilder) noexcept
{// 输出维度与输入维度相同return inputs[0];
}bool LogQuantPlugin::supportsFormatCombination(int pos, const PluginTensorDesc* inOut, int nbInputs, int nbOutputs) noexcept
{// pos: 0 for input 0, 1 for output 0// 我们只支持 FP32 输入和 FP32 输出,且为线性布局(kLINEAR)const auto& desc = inOut[pos];if (desc.format != TensorFormat::kLINEAR) {return false;}if (pos == 0) { // Inputreturn desc.type == DataType::kFLOAT;}if (pos == 1) { // Outputreturn desc.type == DataType::kFLOAT;}return false;
}void LogQuantPlugin::configurePlugin(const DynamicPluginTensorDesc* in, int nbInputs, const DynamicPluginTensorDesc* out, int nbOutputs) noexcept {}size_t LogQuantPlugin::getWorkspaceSize(const PluginTensorDesc* inputs, int nbInputs, const PluginTensorDesc* outputs, int nbOutputs) const noexcept
{// 这个插件不需要额外的工作空间return 0;
}int LogQuantPlugin::enqueue(const PluginTensorDesc* inputDesc, const PluginTensorDesc* outputDesc,const void* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) noexcept
{const float* input = static_cast<const float*>(inputs[0]);float* output = static_cast<float*>(outputs[0]);int64_t count = 1;for (int i = 0; i < inputDesc[0].dims.nbDims; ++i) {count *= inputDesc[0].dims.d[i];}// 配置CUDA核函数启动参数int threads = 256;int blocks = (count + threads - 1) / threads;logQuantKernel<<<blocks, threads, 0, stream>>>(input, output, count, mBase, mScale, mQuantMin, mQuantMax);return cudaPeekAtLastError() == cudaSuccess ? 0 : -1;
}// --- LogQuantPluginCreator Implementation ---PluginFieldCollection LogQuantPluginCreator::mFC{};
std::vector<PluginField> LogQuantPluginCreator::mPluginAttributes;LogQuantPluginCreator::LogQuantPluginCreator()
{mPluginAttributes.clear();mPluginAttributes.emplace_back(PluginField("base", nullptr, PluginFieldType::kFLOAT32, 1));mPluginAttributes.emplace_back(PluginField("scale", nullptr, PluginFieldType::kFLOAT32, 1));mPluginAttributes.emplace_back(PluginField("quant_min", nullptr, PluginFieldType::kFLOAT32, 1));mPluginAttributes.emplace_back(PluginField("quant_max", nullptr, PluginFieldType::kFLOAT32, 1));mFC.nbFields = mPluginAttributes.size();mFC.fields = mPluginAttributes.data();
}const char* LogQuantPluginCreator::getPluginName() const noexcept { return LOG_QUANT_PLUGIN_NAME; }const char* LogQuantPluginCreator::getPluginVersion() const noexcept { return LOG_QUANT_PLUGIN_VERSION; }const PluginField* LogQuantPluginCreator::getFieldNames() noexcept { return mFC.fields; }IPluginV2* LogQuantPluginCreator::createPlugin(const char* name, const PluginFieldCollection* fc) noexcept
{const PluginField* fields = fc->fields;float base = 2.0f, scale = 1.0f, quantMin = -128.0f, quantMax = 127.0f;for (int i = 0; i < fc->nbFields; ++i) {const char* attrName = fields[i].name;if (strcmp(attrName, "base") == 0) {base = *static_cast<const float*>(fields[i].data);} else if (strcmp(attrName, "scale") == 0) {scale = *static_cast<const float*>(fields[i].data);} else if (strcmp(attrName, "quant_min") == 0) {quantMin = *static_cast<const float*>(fields[i].data);} else if (strcmp(attrName, "quant_max") == 0) {quantMax = *static_cast<const float*>(fields[i].data);}}return new LogQuantPlugin(base, scale, quantMin, quantMax);
}IPluginV2* LogQuantPluginCreator::deserializePlugin(const char* name, const void* serialData, size_t serialLength) noexcept
{return new LogQuantPlugin(serialData, serialLength);
}void LogQuantPluginCreator::setPluginNamespace(const char* pluginNamespace) noexcept { mNamespace = pluginNamespace; }const char* LogQuantPluginCreator::getPluginNamespace() const noexcept { return mNamespace.c_str(); }

第3步: CMakeLists.txt - 构建脚本

你需要找到你的TensorRT和CUDA的安装路径。

cmake_minimum_required(VERSION 3.10)
project(LogQuantPluginProject)set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CUDA_STANDARD 14)# 找到CUDA
find_package(CUDA REQUIRED)# 设置TensorRT路径 (根据你的环境修改)
set(TENSORRT_ROOT "/path/to/your/TensorRT-8.x.x.x")# 添加头文件路径
include_directories(${TENSORRT_ROOT}/include ${CUDA_INCLUDE_DIRS})# 添加链接库路径
link_directories(${TENSORRT_ROOT}/lib ${CUDA_LIBRARIES})# --- 编译插件动态库 ---
# 添加源文件
add_library(log_quant_plugin SHARED log_quant_plugin.cu)
# 设置为CUDA可执行文件
set_target_properties(log_quant_plugin PROPERTIES CUDA_ARCHITECTURES "70;75;80;86") # 根据你的GPU架构修改
# 链接TensorRT库
target_link_libraries(log_quant_plugin nvinfer nvinfer_plugin)# --- 编译主程序 ---
add_executable(main main.cpp)
# 链接插件库和TensorRT库
target_link_libraries(main log_quant_plugin nvinfer cudart)

第4步: main.cpp - 使用插件的示例

这个程序会构建一个简单的网络:Input -> LogQuantPlugin -> Output

#include "NvInfer.h"
#include "NvInferPlugin.h"
#include <iostream>
#include <fstream>
#include <vector>
#include <memory>// 日志记录器
class Logger : public nvinfer1::ILogger
{void log(Severity severity, const char* msg) noexcept override{if (severity <= Severity::kWARNING)std::cout << msg << std::endl;}
};// 智能指针辅助函数
template <typename T>
using UniquePtr = std::unique_ptr<T, void (*)(T*)>;void destroy_trt_ptr(nvinfer1::IHostMemory* p) { if (p) p->destroy(); }
void destroy_trt_ptr(nvinfer1::IBuilder* p) { if (p) p->destroy(); }
void destroy_trt_ptr(nvinfer1::IBuilderConfig* p) { if (p) p->destroy(); }
void destroy_trt_ptr(nvinfer1::INetworkDefinition* p) { if (p) p->destroy(); }
void destroy_trt_ptr(nvinfer1::ICudaEngine* p) { if (p) p->destroy(); }
void destroy_trt_ptr(nvinfer1::IExecutionContext* p) { if (p) p->destroy(); }int main()
{Logger logger;// 1. 初始化插件库 (如果需要的话,通常是隐式完成的)// initLibNvInferPlugins(&logger, "");// 2. 创建Builder, Network, ConfigUniquePtr<nvinfer1::IBuilder> builder(nvinfer1::createInferBuilder(logger), destroy_trt_ptr);UniquePtr<nvinfer1::INetworkDefinition> network(builder->createNetworkV2(1U << (int)nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH), destroy_trt_ptr);UniquePtr<nvinfer1::IBuilderConfig> config(builder->createBuilderConfig(), destroy_trt_ptr);config->setMaxWorkspaceSize(1 << 20);// 3. 创建网络// 添加输入层auto input = network->addInput("input", nvinfer1::DataType::kFLOAT, nvinfer1::Dims4{1, 1, 4, 4});// 获取插件创建者auto creator = getPluginRegistry()->getPluginCreator(LOG_QUANT_PLUGIN_NAME, LOG_QUANT_PLUGIN_VERSION);if (!creator) {std::cerr << "Plugin creator not found!" << std::endl;return -1;}// 定义插件参数std::vector<nvinfer1::PluginField> fields;float base = 2.0f, scale = 4.0f, qmin = -128.0f, qmax = 127.0f;fields.emplace_back("base", &base, nvinfer1::PluginFieldType::kFLOAT32, 1);fields.emplace_back("scale", &scale, nvinfer1::PluginFieldType::kFLOAT32, 1);fields.emplace_back("quant_min", &qmin, nvinfer1::PluginFieldType::kFLOAT32, 1);fields.emplace_back("quant_max", &qmax, nvinfer1::PluginFieldType::kFLOAT32, 1);nvinfer1::PluginFieldCollection fc;fc.nbFields = fields.size();fc.fields = fields.data();// 创建插件auto plugin = creator->createPlugin("log_quant_layer", &fc);if (!plugin) {std::cerr << "Failed to create plugin!" << std::endl;return -1;}// 将插件添加到网络auto plugin_layer = network->addPluginV2(&input, 1, *plugin);plugin_layer->getOutput(0)->setName("output");network->markOutput(*plugin_layer->getOutput(0));// 4. 构建引擎UniquePtr<nvinfer1::IHostMemory> serialized_engine(builder->buildSerializedNetwork(*network, *config), destroy_trt_ptr);UniquePtr<nvinfer1::IRuntime> runtime(nvinfer1::createInferRuntime(logger), destroy_trt_ptr);UniquePtr<nvinfer1::ICudaEngine> engine(runtime->deserializeCudaEngine(serialized_engine->data(), serialized_engine->size()), destroy_trt_ptr);UniquePtr<nvinfer1::IExecutionContext> context(engine->createExecutionContext(), destroy_trt_ptr);// 5. 推理// 准备数据const int batch_size = 1;const int size = 1 * 1 * 4 * 4;std::vector<float> input_data = {0.0f, 1.0f, 2.0f, 3.0f,4.0f, 8.0f, 16.0f, 32.0f,-0.5f, -1.5f, -5.0f, -10.0f,64.0f, 100.0f, 128.0f, 256.0f};std::vector<float> output_data(size);// 分配GPU内存void* buffers[2];int input_idx = engine->getBindingIndex("input");int output_idx = engine->getBindingIndex("output");cudaMalloc(&buffers[input_idx], size * sizeof(float));cudaMalloc(&buffers[output_idx], size * sizeof(float));// 拷贝数据到GPUcudaMemcpy(buffers[input_idx], input_data.data(), size * sizeof(float), cudaMemcpyHostToDevice);// 执行推理context->enqueueV2(buffers, 0, nullptr);// 拷贝结果回CPUcudaMemcpy(output_data.data(), buffers[output_idx], size * sizeof(float), cudaMemcpyDeviceToHost);// 6. 打印结果std::cout << "Input Data:" << std::endl;for (int i = 0; i < size; ++i) {std::cout << input_data[i] << " ";if ((i + 1) % 4 == 0) std::cout << std::endl;}std::cout << "\nOutput Data (Quantized):" << std::endl;for (int i = 0; i < size; ++i) {std::cout << output_data[i] << " ";if ((i + 1) % 4 == 0) std::cout << std::endl;}// 7. 释放资源cudaFree(buffers[input_idx]);cudaFree(buffers[output_idx]);return 0;
}

5. 如何编译和运行

  1. 环境准备:确保你已经安装了NVIDIA驱动、CUDA Toolkit和TensorRT。
  2. 修改路径:将 CMakeLists.txt 中的 /path/to/your/TensorRT-8.x.x.x 修改为你的TensorRT安装路径。同时,根据你的GPU型号修改 CUDA_ARCHITECTURES
  3. 编译
    mkdir build
    cd build
    cmake ..
    make -j8
    
    编译成功后,build 目录下会生成 liblog_quant_plugin.somain 可执行文件。
  4. 运行
    build 目录下,确保动态库可以被找到,然后运行主程序。
    export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH
    ./main
    

你将会看到输入数据和经过对数量化层计算后的输出结果。

关键点总结

  • 插件是扩展TensorRT的唯一途径:对于非原生支持的操作,必须编写插件。
  • IPluginV2DynamicExt:是实现支持动态尺寸插件的标准接口。
  • 序列化/反序列化:这是将插件及其参数固化到 .engine 文件中的关键,使得引擎可以被独立加载和执行。
  • enqueue:插件的性能核心,在这里调用高度优化的CUDA代码。
  • supportsFormatCombination:精确定义插件支持的数据类型和内存布局,是配置阶段的“守门员”。
  • 输出类型:本示例为了简化,输入输出都用了FP32。在真实的量化场景中,你可能会希望输出INT8类型。这时,你需要在 getOutputDataTypesupportsFormatCombination 中指定 DataType::kINT8,并且你的CUDA核函数需要将结果写入 int8_t* 类型的输出缓冲区。同时,网络中可能需要在该插件前后搭配 IQuantizeLayerIDequantizeLayer 来处理数据类型的转换和缩放。

文章转载自:

http://Ovg5HvjL.grnhb.cn
http://IHIo9oxi.grnhb.cn
http://87IEYpMU.grnhb.cn
http://JJfy3Cyj.grnhb.cn
http://bYaxO6ad.grnhb.cn
http://08Y9cO46.grnhb.cn
http://aA2N2I2N.grnhb.cn
http://APdrmCKM.grnhb.cn
http://KyGAktTs.grnhb.cn
http://AlZ9Y5SY.grnhb.cn
http://x13eLh4R.grnhb.cn
http://0wnOjOoP.grnhb.cn
http://xfnc5Jqc.grnhb.cn
http://xCgsjIKi.grnhb.cn
http://isnQFZur.grnhb.cn
http://GEG8YeTp.grnhb.cn
http://2DDlNU9Y.grnhb.cn
http://uSWR2RlD.grnhb.cn
http://EzhH8Sbw.grnhb.cn
http://rDg21yzl.grnhb.cn
http://TvTvLQNb.grnhb.cn
http://4TSH7nAQ.grnhb.cn
http://e0PGlSab.grnhb.cn
http://xsUFqG1q.grnhb.cn
http://LHqvwG2m.grnhb.cn
http://7QLX2SYX.grnhb.cn
http://tXFe8Omq.grnhb.cn
http://XZRUD7w3.grnhb.cn
http://ArIOmLWt.grnhb.cn
http://fSLH3ghw.grnhb.cn
http://www.dtcms.com/a/373681.html

相关文章:

  • 【Python】S1 基础篇 P4 if 语句指南
  • 在使用ffmpeg与音转文模型时,会报错音转文stack expects a non-empty Tensor List
  • 苏州ecovadis认证500人内费用多少?
  • 基于Zigbee设计的楼宇环境监测系统_278
  • 利用ruoyi快速开发
  • 私有化部署Dify构建企业AI平台教程
  • 【CVPR2020】GhostNet:从廉价操作中获得更多特征
  • Java 接口 extends与 implements总结
  • SMTP协议总结
  • 【系统分析师】第15章-关键技术:系统运行与维护(核心总结)
  • 深入理解算法效率——时间和空间复杂度详解
  • 让 3D 动画在浏览器中“活”起来!
  • Acrobat/Reader JavaScript 开发:Net.HTTP.Request 的使用与限制
  • QT通过QModbusRtuSerialMaster读写电子秤数据实例
  • 【实战中提升自己】内网安全部署之STP的安全技术部署
  • MYSQL数据库初阶 之 MySQL索引特性1【索引概念】
  • Django入门:框架基础与环境搭建
  • 数据结构题集-第四章-串-基础知识题
  • 【golang学习笔记 gin 】1.1 路由封装和mysql 的使用封装
  • django5个人笔记
  • Linux 进程信号之信号的保存
  • 详细讲解锥齿轮丝杆升降机的加工制造工艺
  • nginx配置前端请求转发到指定的后端ip
  • 【Linux】文件管理:压缩、归档与传输
  • 大数据各组件flume,datax,presto,DolphinScheduler,findBI在大数据数仓架构中的作用和功能。
  • 算法之常见八大排序
  • 某公共资源中心-sm2逆向
  • 数电实验二任务验证指南(开关操作与指示灯观察)
  • Redis Stream:轻量级消息队列深度解析
  • RAG-5-案例1