Transformer从菜鸟到新手(六)

引言

上篇文章介绍了如何在多GPU上分布式训练,本文介绍大模型常用的一种推理加速技术——KV缓存。

KV Cache

KV缓存(KV Cache)是在大模型推理中常用的一种技巧。我们知道在推理阶段,Transformer也只能像RNN一样逐个进行预测,也称为自回归。KV cahce是用在注意力阶段缓存key和value状态,具体的我们可以看图示:

202401090317

上图(灰色区域表示掩码)是在没有KV缓存的情况下,在每一步生成时,我们都在重新计算相同的之前的Token注意力,而实际上我们只想计算新Token的注意力。

比如在最后一步,即第4步时,我们再次计算了之前步骤已经算好的Token注意力Attention1到Attention3,实际上这是没有必要的。

如果我们可以缓存之前计算好的Key和Value,那么就可以不需要这么多重复计算,每次只关注最新Token的注意力:

202401091337

上图(蓝色表示缓存起来的Key或Value)在有KV缓存的情况下,每次只需要传入新的Query,然后计算新的Key和Value,并且与之前的Key和Value缓存矩阵拼接在一起,最后计算出最新Token的注意力。这就是KV缓存的主要思想。可以看到这里不再需要掩码。

这里描述的是自注意力中的KV缓存,如果是交叉注意力那么更简单,因为编码器生成的memory不会改变,因此可以直接缓存memory计算出来的Key和Value矩阵,而不需要拼接。

为了让我们的Transformer能支持KV缓存技术,我们需要进行一些改造。首先对MultiHeadAttention模块动刀,主要修改它的forward方法:

 def forward(self,query: Tensor,key_value: Tensor = None,mask: Tensor = None,past_key_value: Tuple[Tensor] = None,use_cache: bool = False,keep_attentions: bool = False,) -> Tuple[Tensor, Tensor]:"""Args:query (Tensor): (batch_size, q_len, d_model)key_value (Tensor, optional): (batch_size, k_len/v_len, d_model) key and value are same.mask (Tensor, optional): mask for padding or decoder. Defaults to None.past_key_value (Tuple[Tensor], optional): cached past key and value states. Defaults to None.use_cache (bool, optional): whether to use kv cache during inference. Defaults to False.keep_attentions (bool): whether to keep attention weigths or not. Defaults to False.Returns:output (Tensor): (batch_size, q_len, d_model) attention outputpresent_key_value (Tuple[Tensor], optional): Cached present key and value states"""if past_key_value is not None:assert self.is_decoder is True, "Encoder cannot cache past key value states"is_self_attention = key_value is None_query = queryquery = self._transform_and_split(self.q, query)if is_self_attention:# the 'self' attentionkey = self._transform_and_split(self.k, _query, is_key=True) # 即先进行Q/K/V转换,再拆分成多头value = self._transform_and_split(self.v, _query)key, value = self._concat_key_value(key, value, past_key_value) # 分情况拼接最新的key和valueelif past_key_value is None:# the cross attention, key_value is memorykey = self._transform_and_split(self.k, key_value, is_key=True)value = self._transform_and_split(self.v, key_value)else:# if is_self_attention == False and past_key_value is not None# key_value is memory and use cache(past_key_value not None) we do not need to calculate the key and value again because it was cached.# since memory will not change during inference.key, value = past_key_valueif self.is_decoder and use_cache:# cache newest key and valuepresent_key_value = (key, value)else:present_key_value = Noneattn_output = self.attenion(query, key, value, mask, keep_attentions)# Concatconcat_output = self.merge_heads(attn_output)# the final liear# output (batch_size, q_len, d_model)output = self.concat(concat_output)return output, present_key_value

其参数发生了一些变换,由原来的query,key,value变成了query,key_value

首先,这里将keyvalue合并了起来,因为如果是自注意力query=key=value,而如果是交叉注意力key=value=memory,然后我们可以通过判断key_value是否为空来分辨本次计算的是自注意力还是交叉注意力;

其次,增加了两个参数past_key_valueuse_cacheuse_cache表示是否使用kv缓存,而past_key_value代表缓存的kv,注意缓存的k和v是不同的,因为它们经过了Key和Value矩阵映射。

然后我们深入方法内部,注意只有在推理阶段的Decoder中才能使用kv cache。

这里要分两种情况:自注意力和交叉注意力。

如果是自注意力直接使用传入的query就可以计算映射后的query,key,value,见代码行32到37。当使用缓存时,传入的query的长度一定是1,因为我们只需要为最新的query去计算注意力分数,算出一个预测的token。但还是需要当前query对应K和V矩阵映射后的keyvalue,将它们与历史(缓存)的拼接起来去计算新的token。

如果是交叉注意力,即Decoder中第二个注意力模块,其query来自decoder,而key和value(即memory)来自encoder。显然这个memory在整个推理阶段都是一样的,因此只需要计算一次,然后存入past_key_value缓存,后续就不再需要重复计算,对应上面的代码行47。

只有在使用缓存且为Decoder的时候才会缓存最新的key和value。

最后和之前一样计算注意力得分即可。

接下来修改DecoderBlock中的forward代码:

 def forward(self,tgt: Tensor,memory: Tensor,tgt_mask: Tensor = None,memory_mask: Tensor = None,past_key_value: Tuple[Tensor] = None,use_cache: bool = True,keep_attentions: bool = False,) -> Tuple[Tensor, Tensor]:"""Args:tgt (Tensor):   (batch_size, tgt_seq_len, d_model) the (target) sequence to the decoder block.memory (Tensor):  (batch_size, src_seq_len, d_model) the sequence from the last layer of the encoder.tgt_mask (Tensor, optional):  (batch_size, 1, tgt_seq_len, tgt_seq_len) the mask for the tgt sequence.memory_mask (Tensor, optional): (batch_size, 1, 1, src_seq_len) the mask for the memory sequence.past_key_values (Tuple[Tensor], optional): the cached key and value states. Defaults to None.use_cache (bool, optional): whether use kv cache during inference or not. Defaults to False.keep_attentions (bool): whether keep attention weigths or not. Defaults to False.Returns:tgt (Tensor): (batch_size, tgt_seq_len, d_model) output of decoder block"""if past_key_value is not None:# first two elements in the past_key_value tuple are self-attention# past_key_value是一个元组,其中前2个元素为自注意力层的key和value# 后2个元素为交叉注意力层的key和valueself_attn_past_key_value = past_key_value[:2]cross_attn_past_key_value = past_key_value[2:]else:self_attn_past_key_value = Nonecross_attn_past_key_value = Nonex = tgt# 自注意力self_attn_outputs = self._sa_sub_layer(x,tgt_mask,self_attn_past_key_value,use_cache,keep_attentions,)# self attention output and present key value state# x和之前的输出一样,多了一个保存key和value的present_key_value_statex, present_key_value_state = self_attn_outputs# 交叉注意力cross_attn_outputs = self._ca_sub_layer(x,memory,memory_mask,cross_attn_past_key_value,use_cache,keep_attentions,)x = cross_attn_outputs[0]if present_key_value_state is not None:# append the cross-attention key and value states to present key value states   # 拼接注意力和交叉注意力中的key和value,得到元组的4个元素present_key_value_state = present_key_value_state + cross_attn_outputs[1]x = self._ff_sub_layer(x)# 别忘了返回return x, present_key_value_state

其中调用了两个子层对应的方法如下:

def _sa_sub_layer(self,x: Tensor,attn_mask: Tensor,past_key_value: Tensor,use_cache: bool,keep_attentions: bool,
) -> Tensor:residual = xx, present_key_value = self.masked_attention(query=self.norm1(x),past_key_value=past_key_value,use_cache=use_cache,mask=attn_mask,keep_attentions=keep_attentions,)x = self.dropout1(x) + residualreturn x, present_key_value# cross attention sub layer
def _ca_sub_layer(self,x: Tensor,mem: Tensor,attn_mask: Tensor,past_key_value: Tensor,use_cache: bool,keep_attentions: bool,
) -> Tensor:residual = xx, present_key_value = self.cross_attention(query=self.norm2(x),key_value=mem,mask=attn_mask,past_key_value=past_key_value,use_cache=use_cache,keep_attentions=keep_attentions,)x = self.dropout2(x) + residualreturn x, present_key_value

这里改成了默认Pre-LN的形式,即先计算层归一化,最后再进行残差连接。

还有一个非常重要的修改是PositionalEncoding

def forward(self, x: Tensor, position_ids: Union[int, list[int]] = None) -> Tensor:"""Args:x (Tensor): (batch_size, seq_len, d_model) embeddingsposition_ids (Union[int, list[int]]): singe position id or listReturns:Tensor: (batch_size, seq_len, d_model)"""if position_ids is None:position_ids = range(x.size(1))return self.dropout(x + self.pe[:, position_ids, :])

增加了一个参数表示位置id,我们知道如果使用缓存传入的seq_len恒等于1,但实际上它对应的位置ID是不停增加的,若不修改此处,默认通过range(x.size(1))永远只能获取索引等于0时的位置编码,导致表现大幅下降。因此我们要传入当前的位置。

由于缓存只对Decoder生效,因此我们可以直接修改Transformer模块的decode方法:

def decode(self,tgt: Tensor,memory: Tensor,tgt_mask: Tensor = None,memory_mask: Tensor = None,past_key_values: Tuple[Tensor] = None,use_cache: bool = False,keep_attentions: bool = False,
) -> Tensor:"""Args:tgt (Tensor):  (batch_size, tgt_seq_len) the sequence to the decoder.memory (Tensor): (batch_size, src_seq_len, d_model) the  sequence from the last layer of the encoder.tgt_mask (Tensor, optional): (batch_size, 1, 1, tgt_seq_len) the mask for the target sequence. Defaults to None.memory_mask (Tensor, optional): (batch_size, 1, 1, src_seq_len) the mask for the memory sequence. Defaults to None.past_key_values (Tuple[Tensor], optional): the cached key and value states. Defaults to None.use_cache (bool, optional): whether use kv cache during inference or not. Defaults to False.keep_attentions (bool, optional): whether keep attention weigths or not. Defaults to False.Returns:Tensor: output (batch_size, tgt_seq_len, tgt_vocab_size)"""if past_key_values is None:past_key_values = [None] * len(self.decoder.layers)# 未使用缓存则传Noneposition_ids = Noneelse:# when use_cache we only care about the current position# 否则传入当前位置对应的IDposition_ids = past_key_values[0][1].size(2)tgt_embed = self.dec_pos(self.tgt_embedding(tgt), position_ids)# logits (batch_size, tgt_seq_len, d_model)logits, past_key_values = self.decoder(tgt_embed,memory,tgt_mask,memory_mask,past_key_values,use_cache,keep_attentions,)return logits, past_key_values

代码增加了注释,大概意思是如果使用缓存,那么我们需要知道缓存的key或value对应的长度。而刚好seq_len恒等于1,因此不需要增加这个seq_lenpast_key_values[0][1].size(2)的值刚好就是我们想要的位置ID。

最后对贪心解码的实现进行一些小修改:

def _greedy_search(self,src: Tensor,src_mask: Tensor,max_gen_len: int,use_cache: bool,keep_attentions: bool,
):memory = self.transformer.encode(src, src_mask)batch_size = src.shape[0]device = src.device# keep track of which sequences are already finishedunfinished_sequences = torch.ones(batch_size, dtype=torch.long, device=device)decoder_inputs = torch.LongTensor(batch_size, 1).fill_(self.bos_idx).to(device)input_ids = decoder_inputseos_idx_tensor = torch.tensor([self.eos_idx]).to(device)finished = Falsepast_key_values = Nonetgt_mask = None # 使用缓存的情况下可以传None,因为此时query可以看到所有的key。while True:if not use_cache:tgt_mask = self.generate_subsequent_mask(decoder_inputs.size(1), device)outputs = self.transformer.decode(input_ids,memory,tgt_mask=tgt_mask,memory_mask=src_mask,past_key_values=past_key_values,use_cache=use_cache,keep_attentions=keep_attentions,)logits = self.lm_head(outputs[0])past_key_values = outputs[1]next_tokens = torch.argmax(logits[:, -1, :], dim=-1)# finished sentences should have their next token be a pad tokennext_tokens = next_tokens * unfinished_sequences + self.pad_idx * (1 - unfinished_sequences)decoder_inputs = torch.cat([decoder_inputs, next_tokens[:, None]], dim=-1)# set sentence to finished if eos_idx was foundunfinished_sequences = unfinished_sequences.mul(next_tokens.tile(eos_idx_tensor.shape[0], 1).ne(eos_idx_tensor.unsqueeze(1)).prod(dim=0))if use_cache:# only need the last tokensinput_ids = next_tokens[:, None]else:input_ids = decoder_inputs# all sentences have eos_idxif unfinished_sequences.max() == 0:finished = Trueif decoder_inputs.shape[-1] >= max_gen_len:finished = Trueif finished:breakreturn decoder_inputs

在使用缓存的时候 input_ids = next_tokens[:, None],这样保证每次只传入最新预测的Token。

最后在测试集上进行推理来验证下加了kv cache速度提升了多少:

$ python train.py 
source tokenizer size: 32000
target tokenizer size: 32000
Loads cached dataframes.
The model has 93255680 trainable parameters
begin train with arguments: {'d_model': 512, 'n_heads': 8, 'num_encoder_layers': 6, 'num_decoder_layers': 6, 'd_ff': 2048, 'dropout': 0.1, 'max_positions': 5000, 'source_vocab_size': 32000, 'target_vocab_size': 32000, 'attention_bias': False, 'pad_idx': 0, 'dataset_path': 'nlp-in-action/transformers/transformer/data/wmt', 'src_tokenizer_file': 'nlp-in-action/transformers/transformer/model_storage/source.model', 'tgt_tokenizer_path': 'nlp-in-action/transformers/transformer/model_storage/target.model', 'model_save_path': 'nlp-in-action/transformers/transformer/model_storage/best_transformer.pt', 'dataframe_file': 'dataframe.{}.pkl', 'use_dataframe_cache': True, 'cuda': True, 'num_epochs': 40, 'batch_size': 32, 'gradient_accumulation_steps': 1, 'grad_clipping': 0, 'betas': (0.9, 0.98), 'eps': 1e-09, 'label_smoothing': 0, 'warmup_steps': 4000, 'warmup_factor': 0.5, 'only_test': True, 'max_gen_len': 60, 'use_wandb': False, 'patient': 5, 'calc_bleu_during_train': True, 'use_kv_cache': False}
total train steps: 2212000%|                                                                                                                                                                        | 0/1580 [00:00<?, ?it/s]
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1580/1580 [17:25<00:00,  1.51it/s]
TEST loss=0.0021 bleu score: 26.74$ python train.py
source tokenizer size: 32000
target tokenizer size: 32000
Loads cached dataframes.
The model has 93255680 trainable parameters
begin train with arguments: {'d_model': 512, 'n_heads': 8, 'num_encoder_layers': 6, 'num_decoder_layers': 6, 'd_ff': 2048, 'dropout': 0.1, 'max_positions': 5000, 'source_vocab_size': 32000, 'target_vocab_size': 32000, 'attention_bias': False, 'pad_idx': 0, 'dataset_path': 'transformers/transformer/data/wmt', 'src_tokenizer_file': 'transformers/transformer/model_storage/source.model', 'tgt_tokenizer_path': 'transformers/transformer/model_storage/target.model', 'model_save_path': 'transformers/transformer/model_storage/best_transformer.pt', 'dataframe_file': 'dataframe.{}.pkl', 'use_dataframe_cache': True, 'cuda': True, 'num_epochs': 40, 'batch_size': 32, 'gradient_accumulation_steps': 1, 'grad_clipping': 0, 'betas': (0.9, 0.98), 'eps': 1e-09, 'label_smoothing': 0, 'warmup_steps': 4000, 'warmup_factor': 0.5, 'only_test': True, 'max_gen_len': 60, 'use_wandb': False, 'patient': 5, 'calc_bleu_during_train': True, 'use_kv_cache': True}
total train steps: 2212000%|                                                                                                                                                                        | 0/1580 [00:00<?, ?it/s]
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1580/1580 [13:37<00:00,  1.93it/s]
TEST loss=0.0021 bleu score: 26.74

这里加载之前训练效果最好的模型,可以看到计算出来的BLEU 分数都为26.74,使用kv cache耗时(单GPU推理)由17:25降到了13:37,快了接近4分钟。

kv cache实际上是一种空间换时间的技术,那么它会占多大的空间呢?

从上面代码可以看到,我们为每个Token都保存了4个向量,2个k和2个v,那么保存的字节数为:
4 ⋅ 4 ⋅ num_layers ⋅ num_heads ⋅ d_head 4 \cdot 4 \cdot \text{num\_layers} \cdot \text{num\_heads} \cdot \text{d\_head} 44num_layersnum_headsd_head
第一个4表示有4个向量;第二个4表示假设在float-32下需要4个字节;为每层都保存kv cahce;每个向量的大小为 num_heads × d_head \text{num\_heads} \times \text{d\_head} num_heads×d_head

在base设定下(层数=6,d_model=512)批大小等于1,一个Token需要48kb的显存,假设最终生成512个长度的序列时,那么需要24M的显存。看起来不大,但对于大模型的参数量来说,显存占用就显著上升了。

我们这次结合多GPU和KV缓存进行训练:

$ sh train.sh 
Number of GPUs used: 3
Running  DDP on rank 2.0%|          | 0/1844 [00:00<?, ?it/s]Running  DDP on rank 1.0%|          | 0/1844 [00:00<?, ?it/s]Running  DDP on rank 0.
source tokenizer size: 32000
target tokenizer size: 32000
Loads cached train dataframe.
Loads cached dev dataframe.
The model has 93255680 trainable parameters
begin train with arguments: {'d_model': 512, 'n_heads': 8, 'num_encoder_layers': 6, 'num_decoder_layers': 6, 'd_ff': 2048, 'dropout': 0.1, 'max_positions': 5000, 'source_vocab_size': 32000, 'target_vocab_size': 32000, 'attention_bias': False, 'pad_idx': 0, 'dataset_path': 'nlp-in-action/transformers/transformer/data/wmt', 'src_tokenizer_file': 'nlp-in-action/transformers/transformer/model_storage/source.model', 'tgt_tokenizer_path': 'nlp-in-action/transformers/transformer/model_storage/target.model', 'model_save_path': 'nlp-in-action/transformers/transformer/model_storage/best_transformer.pt', 'dataframe_file': 'dataframe.{}.pkl', 'use_dataframe_cache': True, 'cuda': True, 'num_epochs': 40, 'batch_size': 32, 'gradient_accumulation_steps': 1, 'grad_clipping': 0, 'betas': (0.9, 0.98), 'eps': 1e-09, 'label_smoothing': 0, 'warmup_steps': 4000, 'warmup_factor': 0.5, 'only_test': False, 'max_gen_len': 60, 'use_wandb': False, 'patient': 5, 'calc_bleu_during_train': True, 'use_kv_cache': True}
total train steps: 73760
[GPU0] TRAIN  loss=7.033506, learning rate=0.0001612: 100%|██████████| 1844/1844 [03:57<00:00,  7.76it/s]
[GPU1] TRAIN  loss=7.085324, learning rate=0.0001612: 100%|██████████| 1844/1844 [03:57<00:00,  7.76it/s]
[GPU2] TRAIN  loss=6.532835, learning rate=0.0001612: 100%|██████████| 1844/1844 [03:57<00:00,  7.76it/s]0%|          | 0/264 [00:00<?, ?it/s]
| ID | GPU | MEM |
------------------
|  0 |  0% | 22% |
|  1 | 87% | 80% |
|  2 | 83% | 72% |
|  3 | 87% | 74% |
begin evaluate
100%|██████████| 264/264 [00:07<00:00, 36.57it/s]
100%|██████████| 264/264 [00:07<00:00, 36.18it/s]
calculate bleu score for dev dataset
100%|██████████| 264/264 [00:07<00:00, 35.56it/s]
100%|██████████| 264/264 [02:47<00:00,  1.57it/s]
100%|██████████| 264/264 [02:51<00:00,  1.54it/s]
100%|██████████| 264/264 [02:52<00:00,  1.53it/s]
[GPU1] end of epoch   1 [ 421s]| train loss: 8.0776 | valid loss: 7.1336 |  valid bleu_score 0.42
[GPU0] end of epoch   1 [ 421s]| train loss: 8.0674 | valid loss: 7.1126 |  valid bleu_score 0.41
Save model with best bleu score :0.41[GPU0] end of epoch   2 [ 403s]| train loss: 6.5031 | valid loss: 5.8428 |  valid bleu_score 6.66
Save model with best bleu score :6.66[GPU0] end of epoch   3 [ 400s]| train loss: 5.2757 | valid loss: 4.6797 |  valid bleu_score 16.64
Save model with best bleu score :16.64[GPU0] end of epoch   4 [ 400s]| train loss: 4.2989 | valid loss: 4.1087 |  valid bleu_score 21.78
Save model with best bleu score :21.78[GPU0] end of epoch   5 [ 396s]| train loss: 3.7218 | valid loss: 3.8263 |  valid bleu_score 23.51
Save model with best bleu score :23.51[GPU0] end of epoch   6 [ 396s]| train loss: 3.3296 | valid loss: 3.6755 |  valid bleu_score 24.84
Save model with best bleu score :24.84[GPU0] end of epoch   8 [ 391s]| train loss: 2.8033 | valid loss: 3.5605 |  valid bleu_score 25.86
Save model with best bleu score :25.86[GPU0] end of epoch  10 [ 386s]| train loss: 2.4323 | valid loss: 3.5600 |  valid bleu_score 26.43
Save model with best bleu score :26.43[GPU0] end of epoch  11 [ 400s]| train loss: 2.2831 | valid loss: 3.5782 |  valid bleu_score 26.91
Save model with best bleu score :26.91[GPU0] end of epoch  12 [ 390s]| train loss: 2.1463 | valid loss: 3.6085 |  valid bleu_score 26.77[GPU0] end of epoch  13 [ 397s]| train loss: 2.0249 | valid loss: 3.6398 |  valid bleu_score 26.61[GPU0] end of epoch  14 [ 389s]| train loss: 1.9126 | valid loss: 3.6763 |  valid bleu_score 26.41[GPU0] end of epoch  15 [ 388s]| train loss: 1.8102 | valid loss: 3.7161 |  valid bleu_score 26.15| ID | GPU | MEM |
------------------
|  0 |  1% | 22% |
|  1 | 81% | 81% |
|  2 | 80% | 75% |
|  3 | 89% | 89% |[GPU0] end of epoch  16 [ 399s]| train loss: 1.7163 | valid loss: 3.7508 |  valid bleu_score 26.38
stop from early stopping.

基本上每个epoch快了个30秒左右,可以明显的看到第一个epoch训练大概用了3分57秒,但推理时只用了2分50秒左右,并且比上篇文章省了一个epoch。

注意,这里为了性能,虽然设置了随机种子,但并不是完全确定的,即每次结果可能稍微有点不同,如果想实现完全可复现,可参考 https://pytorch.org/docs/stable/notes/randomness.html 。

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

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

相关文章

关于报错 curl: (56) Recv failure: Connection reset by peer

curl ip没问题 curl localhost 则报错 curl: (56) Recv failure: Connection reset by peer 出现这个报错有很多原因, 其中之一就是terminal代理 而关闭代理应用之后, 其实由于配置的终端都是 export指定的代理 所以导致还是一直报错. 通过 curl -v 可以发现 指向了代理ip和…

深入浅出理解SPP、ASPP、DSPP、MDSPP空间金字塔池化系列结构(综合版)

一、参考资料 目标检测&#xff1a;SPP-net SPP原理及实现 金字塔池化系列的理解SPP、ASPP SPP&#xff0c;PPM、ASPP和FPN结构理解和总结 二、空间金字塔池化(SPP) 原始论文&#xff1a;[1] 1. 引言 传统的卷积神经网络中&#xff0c;池化层通常采用固定的池化层级和固定…

使用SpringCache操作Redis缓存数据

SpringCache概念 SpringCache是一个框架&#xff0c;实现了基于注解的缓存功能&#xff0c;只需要简单的加一个注解&#xff0c;就能实现缓存功能。 SpringCache提供了一层抽象&#xff0c;底层可以切换不同的缓存实现&#xff0c;例如&#xff1a; EHCacheCaffeineRedis 使…

2024 年 1 月安全更新修补了 58 个漏洞(Android )

谷歌发布了针对 Android 平台 58 个漏洞的补丁&#xff0c;并修复了 Pixel 设备中的 3 个安全漏洞&#xff0c;拉开了 2024 年的序幕。 Android 2024 年 1 月更新的第一部分以 2024 年 1 月 1 日安全补丁级别发布在设备上&#xff0c;解决了框架和系统组件中的 10 个安全漏洞&…

学习笔记17——通俗易懂的三次握手四次挥手

提供一种博主本人觉得很好理解的三次握手和四次挥手场景&#xff0c;帮助记忆 三次握手过程 初始状态&#xff1a;客户端处于closed状态&#xff0c;服务器处于listen监听转台客户端向服务器发送一个SYN连接请求&#xff0c;并告诉对方自己此时初始化序列号为x&#xff0c;发送…

TYPE-C接口取电芯片介绍和应用场景

随着科技的发展&#xff0c;USB PDTYPE-C已经成为越来越多设备的充电接口。而在这一领域中&#xff0c;LDR6328Q PD取电芯片作为设备端协议IC芯片&#xff0c;扮演着至关重要的角色。本文将详细介绍LDR6328Q PD取电芯片的工作原理、应用场景以及选型要点。 一、工作原理 LDR63…

【昕宝爸爸小模块】HashMap用在并发场景存在的问题

HashMap用在并发场景存在的问题 一、✅典型解析1.1 ✅JDK 1.8中1.2 ✅JDK 1.7中1.3 ✅如何避免这些问题 二、 ✅HashMap并发场景详解2.1 ✅扩容过程2.2 ✅ 并发现象 三、✅拓展知识仓3.1 ✅1.7为什么要将rehash的节点作为新链表的根节点3.2 ✅1.8是如何解决这个问题的3.3 ✅除了…

【SSO】统一授权中心v1.0.0版本正式上线(多租户)

目录 背景 体验 技术栈 菜单 示例 背景 为了方便权限管理、用户登录授权、应用授权等&#xff0c;特地开发了当前的统一授权中心。 体验 邮箱注册即可登录体验 后台系统&#xff1a;https://sso.behappyto.cn/#/switch 技术栈 vue3tsspringbootmybatismysql 菜单 …

SpringBoot用MultipartFile.transferTo传递相对路径的问题

问题描述&#xff1a; 打算给自己的项目添加一个上传文件保存功能&#xff0c;于是我使用MultipartFile.transferTo()来完成这个功能&#xff0c;由于我的项目要部署到服务器&#xff0c;所以我使用了相对路径把上传的文件保存到当前项目的工作目录下&#xff0c;但是报错了&am…

C++上位软件通过Snap7开源库访问西门子S7-200/LOGO PLC/合信M226ES PLC V存储区的方法

前言 在前面例程中谈到了C 通过Snap7开源库S7通信库跟西门子S7-1200PLC/S7-1500PLC以及合信CTMC M226ES PLC/CPU226 PLC通信的方式方法和应用例程。但是遗憾的是Snap7中根据官方资料显示只能访问PLC的 DB区、MB区、C区、T区 、I区、Q区&#xff0c;并没有提到有关如何访问S7-20…

在学习爬虫前的准备

1. 写一个爬虫程序需要分几步 获取网页内容。 我们会通过代码给一个网站服务器发送请求&#xff0c;它会返回给我们网页上的内容。 在我们平时使用浏览器访问服务器内容是&#xff0c;本质上也是向服务器发送一个请求&#xff0c;然后服务器返回网页上的内容。只不过浏览器还会…

K8s Pod详解

1.Pod结构 每个Pod中都可以包含一个或者多个容器&#xff0c;这些容器可以分为两类&#xff1a; 用户程序所在的容器&#xff0c;数量可多可少 Pause容器&#xff0c;这是每个Pod都会有的一个根容器&#xff0c;它的作用有两个&#xff1a; 可以以它为依据&#xff0c;评估整个…

恒创科技:解决Windows服务器磁盘空间不足的问题

​  服务器硬盘的大小是决定空间是否充足的主要因素。但在日常使用中&#xff0c;服务器和网站备份会消耗大量存储空间&#xff0c;如果维护不当&#xff0c;最终将耗尽您的容量。同样&#xff0c;日志文件、临时文件和数据库可以在硬盘驱动器上或回收站中无休止地建立。当您…

手把手教你升级GPT-4,内附详细步骤

目录 1、先介绍一下 GPT 升级 2、第一种: 免费升级 支付宝购买礼品卡给美区 apple id 充值 3、第二种&#xff1a;5分钟快速升级 方法 平时我会在朋友圈分享一些利用 GPT-4 画的图片&#xff0c;比如下面这个扑克牌风格的"黑红小狗武士"。 用 GPT-4 做绘画仅仅是…

如何使用宝塔面板部署Inis博客并实现无公网ip环境远程访问

文章目录 前言1. Inis博客网站搭建1.1. Inis博客网站下载和安装1.2 Inis博客网站测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 3. 公网访问测试总…

大创项目推荐 深度学习机器视觉车道线识别与检测 -自动驾驶

文章目录 1 前言2 先上成果3 车道线4 问题抽象(建立模型)5 帧掩码(Frame Mask)6 车道检测的图像预处理7 图像阈值化8 霍夫线变换9 实现车道检测9.1 帧掩码创建9.2 图像预处理9.2.1 图像阈值化9.2.2 霍夫线变换 最后 1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分…

ElasticSearch _update_by_query

根据查询条件进行数据更新 UPDATE job_call SET admin_id 0 WHERE admin_id 283; kibana.png 1.其中红色框的位置为query的内容&#xff0c;对应为SQL语句中的WHERE admin_id 283 2.划红色线的位置为修改内容&#xff0c;对应SQL中的SET admin_id 0。如果是更新多个字段s…

手轮脉冲平滑处理笔记

这是一个求手脉倍率((Hw_Control.mult_ratio)与手脉脉冲计数延迟次数即累计过去n次的平均值(Hw_Control.lag_num)之间关系算法的计算过程笔记文档 1、已知 mult_ratio=1时 lag_num=10; mult_ratio=10时 lag_num=20; .mult_ratio==100时 lag_num=30; 以此类推 2、设lag_num…

开关电源PFC电路原理详解及matlab仿真

PFC全称“Power Factor Correction”&#xff0c;意为“功率因数校正”。PFC电路即能对功率因数进行校正&#xff0c;或者说能提高功率因数的电路。是开关电源中很常见的电路。 在电学中&#xff0c;功率因数PF指有功功率P&#xff08;单位w&#xff09;与视在功率S&#xff08…

每日学习更新(LQR+iLQR)

一直想更新一下根据cost to go来推导LQR&#xff0c;之前的话可能会直接套问题&#xff0c;但是对于理论有些困惑&#xff0c;正好最近在学习ilqr轨迹生成/优化&#xff0c;因此来推一下公式&#xff0c;以下参考B站Dr_CAN&#xff0c;链接如下&#xff1a; 【最优控制】5_线性…