模型量化操作————GPTQ和AWQ量化 - Big-Yellow

news/2025/11/1 11:07:26/文章来源:https://www.cnblogs.com/Big-Yellow/p/19182016

From:https://www.big-yellow-j.top/posts/2025/10/11/Quantized.html

模型量化技术

简单了解几个概念:
量化:是一种模型压缩的常见方法,将模型权重从高精度(如FP16或FP32)量化为低比特位(如INT8、INT4)。常见的量化策略可以分为PTQ和QAT两大类。
量化感知训练(Quantization-Aware Training):在模型训练过程中进行量化,一般效果会更好一些,但需要额外训练数据和大量计算资源。
后量化(Post-Training Quantization, PTQ):在模型训练完成后,对模型进行量化,无需重新训练。
因此对于量化过程总结为:将数值精度进行“校准”(比如FP32转化到INT8,两种表述范围不同,因此就需要将前者校准到后者范围),对“校准”数据进行精度转化。对于线性量化下,浮点数与定点数之间的转换公式如下:\(Q=\frac{R}{S}+Z;R=(Q-Z)*S\),其中R 表示量化前的浮点数、Q 表示量化后的定点数、S(Scale)表示缩放因子的数值、Z(Zero)表示零点的数值。
模型量化具体实现过程(直接使用:https://zhuanlan.zhihu.com/p/646210009中的描述):
对称量化中,零点 Z = 0,一般不记录,我们只需要关心如何求解 Scale。由于 weight 几乎不存在异常值,因此我们可以直接取 Scale 为一个 layer 或 block 内所有参数的最大绝对值,于是所有的参数都在 [-1, 1] 的区间内。随后,这些参数将找到最近的量化格点,并转化成定点数。

GPTQ量化技术

GPTQ[1]是一种用于大型语言模型(LLM)的后训练量化技术。它通过将模型权重从高精度(如FP16或FP32)压缩到低比特(如3-4位整数)来减少模型大小和内存占用,同时保持较高的推理准确性。一般而言对于量化过程为:对于给定的权重矩阵\(W\in R^{n\times m}\)量化过程就是需要找到一个低比特的矩阵\(\hat{W}\)使得:

\[\min_{\hat{w}}\Vert WX-\hat{W}X\Vert^2_F \]

其中\(X\)为输入向量,\(\Vert. \Vert_F\)为Frobenius范数。按照论文里面的描述GPTQ整个过程为:

对于具体数学原理的描述参考文章[2][3](数学原理推荐直接看:GPTQ详细解读),简单总结一下上面过程就是:1、每行独立计算二阶海森矩阵。2、每行按顺序进行逐个参数量化,从而可以并行计算。3、按block维度进行更新,对剩余参数进行延迟更新弥补。4、对逆海森矩阵使用cholesky分解,等价消除迭代中的矩阵更新计算。它的核心流程其实就是量化-补偿-量化-补偿的迭代(具体过程见流程图中内部循环:首先量化\(W_{:,j}\),而后去计算误差并且补充到 \(W_{:,j:(i+B)}\)),具体的代码实现过程(官方GPTQ-Github)主要是对其中LlamaAttention和LlamaMLP层中的Linear层权重进行量化。代码处理过程[4]
首先、计算Hessian矩阵(因为后续计算损失和补偿权重需要,因此提前计算矩阵)
这个矩阵近似:\(H_F=2X_FX_F^T\)\(X\)经过前面几层神经网络之后,到达被量化层的激活)。实现方式是在每一层Layer上注册hook,通过hook的方式在layer forward后使用calibration data的input来生成Hessian矩阵,这种计算方式常见于量化流程中校准数据的处理

def add_batch(name):def tmp(_, inp, out):# 假设过程为:x → Linear(W) → ReLU# x →inp[0].data Linear层输出→outgptq[name].add_batch(inp[0].data, out.data)return tmp
handles = []
# 添加hook
for name in subset:handles.append(subset[name].register_forward_hook(add_batch(name)))
# 处理岩本计算数据
for j in range(args.nsamples):outs[j] = layer(inps[j].unsqueeze(0), attention_mask=attention_mask, position_ids=position_ids)[0]
# 去除hook
for h in handles:h.remove()

add_batch中具体为了利用所有的校准数据,这里通过迭代的方式将每组数据计算的Hessian矩阵值进行求和然后取平均,代码实现是迭代逐渐平均叠加的过程,Hessian矩阵求解公式:\(H_F=2X_FX_F^T\)

# 假设过程为:x → Linear(W) → ReLU
# x →inp[0].data Linear层输出→out
#gptq[name].add_batch(inp[0].data, out.data)
def add_batch(self, inp, out):...if len(inp.shape) == 2:inp = inp.unsqueeze(0)tmp = inp.shape[0]if isinstance(self.layer, nn.Linear) or isinstance(self.layer, transformers.Conv1D):if len(inp.shape) == 3:inp = inp.reshape((-1, inp.shape[-1]))inp = inp.t()if isinstance(self.layer, nn.Conv2d):unfold = nn.Unfold(self.layer.kernel_size,dilation=self.layer.dilation,padding=self.layer.padding,stride=self.layer.stride)inp = unfold(inp)inp = inp.permute([1, 0, 2])inp = inp.flatten(1)self.H *= self.nsamples / (self.nsamples + tmp)self.nsamples += tmpinp = math.sqrt(2 / self.nsamples) * inp.float()self.H += inp.matmul(inp.t())

其次、逐层weight量化

for name in subset:gptq[name].fasterquant(percdamp=args.percdamp, groupsize=args.groupsize, actorder=args.act_order, static_groups=args.static_groups)quantizers['model.layers.%d.%s' % (i, name)] = gptq[name].quantizergptq[name].free()

主要是通过逐层使用fasterquant方法作为入口来进行量化处理。fasterquant 用层的权重矩阵 W 和之前收集到的激活 Gram(或近似 Hessian)H 来做按列(按 block)贪心量化。它先把 H 经过阻尼并通过 Cholesky/逆操作得到用于投影/补偿的因子(称为 Hinv),然后按 block 内逐列量化:对第 j 列量化后计算误差 e_j,用 Hinv 的相应行/列把这个误差按 Schur 补方式投影/传播到该 block 内剩余列并在 block 外一次性传播到后续列,从而实现 GPTQ 的误差补偿策略。在fasterquant方法中主要进行了量化的计算过程,具体实现过程为(核心代码):

def fasterquant(self, blocksize=128, percdamp=.01, groupsize=-1, actorder=False, static_groups=False
):W = self.layer.weight.data.clone()if isinstance(self.layer, nn.Conv2d):W = W.flatten(1)if isinstance(self.layer, transformers.Conv1D):W = W.t()W = W.float()tick = time.time()if not self.quantizer.ready():self.quantizer.find_params(W, weight=True)# self.H 是上一步中计算得到的Hessian矩阵H = self.Hdel self.Hdead = torch.diag(H) == 0H[dead, dead] = 1W[:, dead] = 0...# 初始化 losses 0矩阵Losses = torch.zeros_like(W)Q = torch.zeros_like(W)damp = percdamp * torch.mean(torch.diag(H))diag = torch.arange(self.columns, device=self.dev)H[diag, diag] += dampH = torch.linalg.cholesky(H)H = torch.cholesky_inverse(H)H = torch.linalg.cholesky(H, upper=True)Hinv = H# 逐Block处理# self.columns = W.shape[1]for i1 in range(0, self.columns, blocksize):i2 = min(i1 + blocksize, self.columns)count = i2 - i1W1 = W[:, i1:i2].clone()Q1 = torch.zeros_like(W1)Err1 = torch.zeros_like(W1)Losses1 = torch.zeros_like(W1)Hinv1 = Hinv[i1:i2, i1:i2]# Block内部量化for i in range(count):w = W1[:, i]d = Hinv1[i, i]if groupsize != -1:if not static_groups:if (i1 + i) % groupsize == 0:self.quantizer.find_params(W[:, (i1 + i):(i1 + i + groupsize)], weight=True)else:idx = i1 + iif actorder:idx = perm[idx]self.quantizer = groups[idx // groupsize]q = quantize(w.unsqueeze(1), self.quantizer.scale, self.quantizer.zero, self.quantizer.maxq).flatten()Q1[:, i] = qLosses1[:, i] = (w - q) ** 2 / d ** 2err1 = (w - q) / dW1[:, i:] -= err1.unsqueeze(1).matmul(Hinv1[i, i:].unsqueeze(0))Err1[:, i] = err1Q[:, i1:i2] = Q1Losses[:, i1:i2] = Losses1 / 2W[:, i2:] -= Err1.matmul(Hinv[i1:i2, i2:])torch.cuda.synchronize()...if actorder:Q = Q[:, invperm]if isinstance(self.layer, transformers.Conv1D):Q = Q.t()self.layer.weight.data = Q.reshape(self.layer.weight.shape).to(self.layer.weight.data.dtype)

对于上面过程主要是看两个for循环的里面内容,首先第一个for循环去根据block去将权重矩阵W进行分块拆分W1 = W[:, i1:i2].clone()),接下来第二个for循环依次去对第1块中每列进行量化,第i列进行量化(quantize)处理(q = quantize(...)),而后去计算loss并且去对其他的列(i:)计算W1[:, i:] -= err1.unsqueeze(1).matmul(Hinv1[i, i:].unsqueeze(0)),在处理完毕第1块之后再去将后面块的列进行误差补偿W[:, i2:] -= Err1.matmul(Hinv[i1:i2, i2:])),这样整个过程就完成了。

# 量化函数
def quantize(x, scale, zero, maxq):if maxq < 0:return (x > scale / 2).float() * scale + (x < zero / 2).float() * zeroq = torch.clamp(torch.round(x / scale) + zero, 0, maxq)return scale * (q - zero)

最后、量化模型保存
之前的步骤中量化和反量化后计算lose都是浮点位数的,所以并没有生成wbit位format的数值内容,在llama_pack方法中通过model和之前得到的quantizer(scale, zero)来生成wbit位数表达格式的量化模型,其定义如下所示

def llama_pack3(model, quantizers):layers = find_layers(model)layers = {n: layers[n] for n in quantizers}make_quant3(model, quantizers)qlayers = find_layers(model, [Quant3Linear])for name in qlayers:quantizers[name] = quantizers[name].cpu()# 使用 Quant3Linear 进行pack处理qlayers[name].pack(layers[name], quantizers[name].scale, quantizers[name].zero)return model
# 将model中每一层都替换为 Quant3Linear
def make_quant3(module, names, name='', faster=False):if isinstance(module, Quant3Linear):returnfor attr in dir(module):tmp = getattr(module, attr)name1 = name + '.' + attr if name != '' else attrif name1 in names:setattr(module, attr, Quant3Linear(tmp.in_features, tmp.out_features, faster=faster))for name1, child in module.named_children():make_quant3(child, names, name + '.' + name1 if name != '' else name1, faster=faster)
...
if args.wbits < 16 and not args.nearest:quantizers = llama_sequential(model, dataloader, DEV)
if args.save:llama_pack3(model, quantizers)

其中quantizers来自量化后的返回,它是一个dict里面保存了每一个层和它对应的quantizer、scale、zero、group_idx等信息,其中quantizer是layer-level的,zero和scale是group-level的。

quantizers的结果为:quantizers['model.layers.%d.%s' % (i, name)] = (gptq[name].quantizer.cpu(), scale.cpu(), zero.cpu(), g_idx.cpu(), args.wbits, args.groupsize)

Quant3Linear具体处理过程(代码),通过qweight、zeros和scales、bias等属性来保存量化后的低比特信息。:

# qlayers[name].pack(layers[name], quantizers[name].scale, quantizers[name].zero)
class Quant3Linear(nn.Module): def __init__(self, infeatures, outfeatures, faster=False):super().__init__()self.register_buffer('zeros', torch.zeros((outfeatures, 1)))self.register_buffer('scales', torch.zeros((outfeatures, 1)))self.register_buffer('bias', torch.zeros(outfeatures))self.register_buffer('qweight', torch.zeros((infeatures // 32 * 3, outfeatures), dtype=torch.int))self.faster = fasterdef pack(self, linear, scales, zeros):self.zeros = zeros * scalesself.scales = scales.clone()if linear.bias is not None:self.bias = linear.bias.clone()intweight = torch.round((linear.weight.data + self.zeros) / self.scales).to(torch.int)intweight = intweight.t().contiguous()intweight = intweight.numpy().astype(np.uint32)qweight = np.zeros((intweight.shape[0] // 32 * 3, intweight.shape[1]), dtype=np.uint32)i, row = 0, 0while row < qweight.shape[0]:# 把 32 个 3-bit 整数按位连续打包到 3 个 uint32...qweight = qweight.astype(np.int32)self.qweight = torch.from_numpy(qweight) 

对于上述打包(3-bit打包)处理过程为:qweight = np.zeros((intweight.shape[0] // 32 * 3, intweight.shape[1]), dtype=np.uint32)每 32 个 intweight 的行使用 3 个 uint32 行来存储,不过值得注意的是以 int32 的形式存储量化权重,但这 并不代表每个权重占 32 bit。这里的 int32 是一个打包容器(bit-packing container),里面塞了多个低 bit(比如 3 bit)的权重值。

AWQ量化技术

AWQ量化[5](逐层量化方法,需要每层的输入激活来计算 scale 和 clip 值)是一种基于激活值分布挑选显著权重进行量化的方法,其不依赖于任何反向传播或重建,因此可以很好地保持LLM在不同领域和模式上的泛化能力,而不会过拟合到校准集,属训练后量化大类,论文里面出发点就是模型的权重并不同等重要,仅有0.1%-1%的小部分显著权重对模型输出精度影响较大。因此如果能有办法只对0.1%~1%这一小部分权重保持原来的精度(FP16),对其他权重进行低比特量化,就可以在保持精度几乎不变的情况下,大幅降低模型内存占用,并提升推理速度。

但是如果部分用FP16而其他的用INT3这样就会导致硬件上存储困难(图b情况),因此作者使用的操作就是:对所有权重均进行低比特量化,但是,在量化时,对于显著权重乘以较大的scale,相当于降低其量化误差;同时,对于非显著权重,乘以较小的scale,相当于给予更少的关注。因此代码关注点就是找到这个scale值

基于激活值分布挑选方法激活值指的是与权重矩阵运算的输入值,比如说:\(V=W_vX\)其中的 \(X\)就是权重 \(W_v\)的激活值,按激活值绝对值大小由大到小排序,绝对值越大越显著,选择前0.1%~1%的元素作为显著权重。
具体代码过程(Github-Code)

首先是获取 模型第一层的输入激活值,供后续的逐层量化使用,代码整体流程如下(核心代码格式):

@torch.no_grad()
def run_awq(model,enc,w_bit,q_config,n_samples=512,seqlen=512,auto_scale=True,mse_range=True,calib_data="pileval",):...layers = get_blocks(model)samples = get_calib_dataset(...)# 得到第一层的激活值inps = []layer_kwargs = {}layers[0] = layers[0].cuda()...class Catcher(nn.Module):def __init__(self, module):super().__init__()self.module = moduledef forward(self, inp, **kwargs):inps.append(inp)layer_kwargs.update(kwargs)raise ValueErrorlayers[0] = Catcher(layers[0])try:if model.__class__.__name__ == "LlavaLlamaModel":model.llm(samples.to(next(model.parameters()).device))...except ValueError:pass...layers[0] = layers[0].moduleinps = inps[0]layers[0] = layers[0].cpu()...

而后、逐层进行量化处理,在AWQ量化过程中需要记录两部分量化值scale(auto_sclae.py) 和 clip(auto_clip.py)两部分具体源码处理过程都是相似的先去计算scale值而后将scale值应用,在计算两部分值之前和GPTQ处理相似去记录forward过程,具体代码为:

for i in tqdm.tqdm(range(len(layers))):layer = layers[i]layer = layer.cuda()named_linears = get_named_linears(layer)def cache_input_hook(m, x, y, name, feat_dict):x = x[0]x = x.detach().cpu()feat_dict[name].append(x)input_feat = defaultdict(list)handles = []for name in named_linears:handles.append(named_linears[name].register_forward_hook(functools.partial(cache_input_hook, name=name, feat_dict=input_feat)))inps = inps.to(next(layer.parameters()).device)inps = layer(inps, **layer_kwargs)[0]for h in handles:h.remove()input_feat = {k: torch.cat(v, dim=0) for k, v in input_feat.items()}

其中cache_input_hook过程就是直接记录每层layer中的linear层的输入值并且将其记录到input_feat中。
scale处理过程代码如下:

elif isinstance(module, (LlamaDecoderLayer, Qwen2DecoderLayer)):# attention inputscales_list.append(_auto_get_scale(prev_op=module.input_layernorm,layers=[module.self_attn.q_proj,module.self_attn.k_proj,module.self_attn.v_proj,],inp=input_feat["self_attn.q_proj"],module2inspect=module.self_attn,kwargs=module_kwargs,))
'''
_auto_get_scale 中核心逻辑是使用 search_module_scale 并且其中4个参数分别对应
block = module2inspect=module.self_attn 
linears2scale = [module.self_attn.q_proj, module.self_attn.k_proj, module.self_attn.v_proj]
x = input_feat["self_attn.q_proj"]
'''
def _search_module_scale(block, linears2scale: list, x, kwargs={}):# block:对应block linears2scale:对应线性层x = x.to(next(block.parameters()).device)# 记录未量化的输出结果with torch.no_grad():org_out = block(x, **kwargs)...x_max = get_act_scale(x) # x.abs().view(-1, x.shape[-1]).mean(0)best_error = float("inf")best_ratio = -1best_scales = Nonen_grid = 20history = []org_sd = {k: v.cpu() for k, v in block.state_dict().items()}for ratio in range(n_grid):ratio = ratio * 1 / n_gridscales = x_max.pow(ratio).clamp(min=1e-4).view(-1)scales = scales / (scales.max() * scales.min()).sqrt()for fc in linears2scale:fc.weight.mul_(scales.view(1, -1).to(fc.weight.device))fc.weight.data = w_quantize_func(fc.weight.data) / (scales.view(1, -1))out = block(x, **kwargs)if isinstance(out, tuple):out = out[0]loss = ((org_out - out).float().pow(2).mean().item())history.append(loss)is_best = loss < best_errorif is_best:best_error = lossbest_ratio = ratiobest_scales = scales# 恢复到最初状态block.load_state_dict(org_sd)...best_scales = best_scales.view(-1)...return best_scales.detach()

对所有权重均进行低比特量化,但是,在量化时,对于显著权重乘以较大的scale,相当于降低其量化误差;同时,对于非显著权重,乘以较小的scale,相当于给予更少的关注

其实对于上面过程就是直接通过网格搜索策略通过得到的x_max=x.abs().view(-1, x.shape[-1]).mean(0)去不断尝试scales去让loss最小,从而得到scale值。对于其中的量化处理过程w_quantize_func,核心是计算 \(q=clip(round(\frac{w}{s}​)+z,q_{min}​,q_{max}​)\)

'''
w_quantize_func(fc.weight.data) / (scales.view(1, -1))
w 对应 fc.weight.data) / (scales.view(1, -1)
'''
def pseudo_quantize_tensor(w, n_bit=8, zero_point=True, q_group_size=-1, inplace=False, get_scale_zp=False):org_w_shape = w.shapeif q_group_size > 0:assert org_w_shape[-1] % q_group_size == 0w = w.reshape(-1, q_group_size)assert w.dim() == 2if zero_point: max_val = w.amax(dim=1, keepdim=True)min_val = w.amin(dim=1, keepdim=True)max_int = 2**n_bit - 1min_int = 0scales = (max_val - min_val).clamp(min=1e-5) / max_intzeros = (-torch.round(min_val / scales)).clamp_(min_int, max_int)else:  ... # 对称量化...if inplace:...else:w = (torch.clamp(torch.round(w / scales) + zeros, min_int, max_int) - zeros) * scalesassert torch.isnan(w).sum() == 0w = w.reshape(org_w_shape)if get_scale_zp:...else:return w

对于上面过程总结就是:把 w 线性映射到一个由 bit 位数(n_bit)决定的固定整数区间(q_min 到 q_max),其中scale 决定缩放比例,zero_point 决定映射偏移

总结

GPTQ量化技术总结:核心流程其实就是量化-补偿-量化-补偿的迭代,首先通过对模型权重\(W\)首先去对\(W\)进行分块拆分得到不同的block再去到每一个block里面去按照每i列进行量化(quantize)处理(q = quantize(...)),而后去计算loss并且去对其他的列(i:)计算W1[:, i:] -= err1.unsqueeze(1).matmul(Hinv1[i, i:].unsqueeze(0)),在处理完毕第1块之后再去将后面块的列进行误差补偿(W[:, i2:] -= Err1.matmul(Hinv[i1:i2, i2:])),这样就得到了scales, zeros这信息,在去使用这些信息去对模型权重进行转化intweight = torch.round((linear.weight.data + self.zeros) / self.scales).to(torch.int),最后就是用32 个intweight的行使用 3 个 uint32 行来存储,推理过程的话:\(y = Wx + b\rightarrow y≈x(s_j(q-z_j))+b\)
AWQ量化技术总结:核心流程就是对所有权重均进行低比特量化,但是,在量化时,对于显著权重乘以较大的scale,相当于降低其量化误差;同时,对于非显著权重,乘以较小的scale,相当于给予更少的关注,对于这个scale值的寻找直接计算每一层的输入“激活值”(x.abs().view(-1, x.shape[-1]).mean(0))而后对这个激活值不断进行scale处理将其通过w_quantize_func操作应用到模型的层上进而得到量化后的模型权重,然后去计算和没有量化的权重loss得到最佳scale

代码操作

Github-code
模型ONNX部署技术

直接使用llmcompressor来量化模型(具体地址:llmcompressor)支持量化类型:

参考


  1. https://github.com/IST-DASLab/gptq ↩︎

  2. https://zhuanlan.zhihu.com/p/646210009 ↩︎

  3. https://zhuanlan.zhihu.com/p/629517722 ↩︎

  4. https://zhuanlan.zhihu.com/p/697860995 ↩︎

  5. https://arxiv.org/pdf/2306.00978 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/952658.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

faust分组ui功能

faust分组ui功能hgroup("label",x)hgroup Primitive The hgroup primitive implements a horizontal group. A group contains other UI elements that can also be groups. hgroup is not a signal process…

CSP-S前集训总结

2025-10-16 战绩:100+100+65+0,总榜并列 rk1。 A.小Z爱计数 简单贪心,按时间排序。对于相邻两个限制,要么直接走去,要么有归零。判断一下就行了。 时间复杂度 \(O(n\log n)\)。 #include<bits/stdc++.h> us…

在AI技术快速实现创意的时代,挖掘用户真实需求成为关键突破点——某知名舆情分析系统需求洞察

本文通过对某知名舆情分析系统的功能特性和用户反馈进行深入分析,揭示了在AI技术日益成熟的背景下,用户对系统性能、使用体验和功能完善度的真实需求,为产品优化提供了明确方向。a.内容描述核心功能定位:该系统是一…

ICPC注意事项

VP 血的教训,下次不要当战犯了。每次都要取模 1e18+10->(long long)1e18 +10

时序数据库-InfluxDB - LLj

一、介绍 InfluxDB是一种时序数据库,时序数据库全程时间序列数据库(Time Series Database,TSDB,用于存储大量基于时间的数据,时序数据(Time Series Data)指的是一系列基于时间的数据,为处理和分析带有时间戳的…

2025年质量好的航空充气密封圈厂家最新推荐排行榜

2025年质量好的航空充气密封圈厂家最新推荐排行榜 航空充气密封圈作为飞行器、地面保障设备及特种装备的核心部件,其性能直接影响安全性与可靠性。随着国产航空产业链的升级,具备高耐候性、抗压性和长寿命的密封圈需…

2025年质量好的非开挖电力管用户好评厂家排行

2025年质量好的非开挖电力管用户好评厂家排行 非开挖电力管作为现代电力工程中的重要材料,广泛应用于城市电网改造、通信工程、农田灌溉等领域。其优势在于施工便捷、环保高效,能够减少对地面的破坏,降低施工成本。…

2025年口碑好的酚醛胶行业内口碑厂家排行榜

2025年口碑好的酚醛胶行业内口碑厂家排行榜 酚醛胶作为一种高性能粘合剂,广泛应用于木材加工、建筑、汽车、电子等领域。随着市场需求的不断增长,酚醛胶行业涌现出一批技术领先、品质卓越的企业。本文根据产品质量、…

基于Java+Springboot+Vue开发的大学生反诈视频宣传系统源码+运行步骤

项目简介该项目是基于Java+Springboot+Vue开发的反诈视频宣传系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过…

Docker 部署 openEuler 教程及常见问题解决

openEuler 作为华为主导的开源 Linux 发行版,以“多架构适配、云原生友好、长期稳定支持”为核心优势,在企业级服务器、边缘计算、云原生场景中应用广泛。通过 Docker 部署 openEuler,能将其系统优势与容器化的“环…

2025年评价高的高铁桥梁垫块厂家最新权威推荐排行榜

2025年评价高的高铁桥梁垫块厂家最新权威推荐排行榜 高铁桥梁垫块作为高铁建设中的关键部件,其质量直接关系到桥梁的安全性和耐久性。随着我国高铁网络的不断扩展,市场对高强度、高精度桥梁垫块的需求持续增长。202…

图中连通区域集合的获取

std::vector<std::vector<int>> getCompGroups(const std::vector<std::vector<int>>& adj) {int n = adj.size();std::vector<char> vis(n, 0);std::vector<std::vector<int…

2025年专业的电加热管厂家最新权威推荐排行榜

2025年专业的电加热管厂家最新权威推荐排行榜 在工业制造、家用电器、新能源等领域,电加热管作为核心热传导元件,其性能与可靠性直接影响设备寿命和能效。随着技术进步和市场需求升级,具备高精度、耐腐蚀、防爆特性…

2025年热门的碳化蒸笼用户好评厂家排行

2025年热门的碳化蒸笼用户好评厂家排行 随着健康饮食理念的普及,碳化蒸笼因其天然环保、耐高温、防霉防蛀等优势,成为现代厨房的优选。2025年,碳化蒸笼市场涌现出一批以品质和创新著称的厂家,本文基于用户真实反馈…

2025年靠谱的给水管设备厂家推荐及选购指南

2025年靠谱的给水管设备厂家推荐及选购指南 在建筑、市政工程、农业灌溉等领域,给水管设备的质量直接关系到供水系统的安全性和使用寿命。选择一家技术先进、产品可靠的给水管设备厂家至关重要。2025年,随着新材料和…

2025年优质的仪器计量校准厂家推荐及采购参考

2025年优质的仪器计量校准厂家推荐及采购参考 在工业生产、科研实验和质量控制领域,仪器计量校准的准确性直接影响产品质量和检测数据的可靠性。选择一家专业、权威的计量校准服务商至关重要。2025年,市场上涌现出众…

2025年比较好的45三折轨厂家实力及用户口碑排行榜

2025年比较好的45三折轨厂家实力及用户口碑排行榜 随着家居五金行业的快速发展,45三折轨因其高承重、静音缓冲、耐用性强等特点,成为现代家居橱柜、衣柜等领域的核心配件。2025年,消费者对五金件的品质要求进一步提…

一对一聊天软件源码,jwt登陆校验携带token - 云豹科技

一对一聊天软件源码,jwt登陆校验携带tokenjwt登陆注册jwt概念token是不需要存储在数据库的,只需要后台生成密钥,当客户端发送过来请求时那么就把token塞在请求体或者头中,客户端接收到token时那么就存储在localStor…

2025年评价高的醪糟甜酒酿厂家最新热销排行

2025年评价高的醪糟甜酒酿厂家最新热销排行 醪糟(甜酒酿)作为中国传统发酵食品,凭借其独特的风味和营养价值,近年来在健康饮食风潮中备受青睐。2025年,随着消费者对品质要求的提升,具备传统工艺与现代技术结合的…

2025年耐用的电加热导热油炉厂家最新权威推荐排行榜

2025年耐用的电加热导热油炉厂家最新权威推荐排行榜电加热导热油炉作为工业生产中重要的热能设备,其性能、耐用性和安全性直接关系到企业的生产效率和运营成本。随着2025年工业4.0时代的全面到来,市场对高效、智能、…