vllm源码解析(五):LLM模型推理

news2024/9/17 7:18:46

八 模型推理细节探索

8.1 回顾下step的流程

在这里插入图片描述

    def step(self) -> List[Union[RequestOutput, EmbeddingRequestOutput]]:
        # 多GPU并行推理时走AsyncLLMEngine分支。如果进入当前LLMEngine,性能会下降,这里会抛出异常。
        if self.parallel_config.pipeline_parallel_size > 1:
            raise NotImplementedError(
                    "Pipeline parallelism is only supported through AsyncLLMEngine "
                    "as performance will be severely degraded otherwise.")

        # 上述if判断表明,只有一个GPU可用。因此self.scheduler也只有一个元素,是当前GPU的调度
        # 该函数调用改变调度的内部状态(self.running、self.swapped 和 self.waiting)
        seq_group_metadata_list, scheduler_outputs = self.scheduler[0].schedule()

        if not scheduler_outputs.is_empty():
            finished_requests_ids = self.scheduler[0].get_and_reset_finished_requests_ids()
            execute_model_req = ExecuteModelRequest(...)

            output = self.model_executor.execute_model(execute_model_req=execute_model_req)
        else:
            output = []

        request_outputs = self._process_model_outputs(
                output, scheduler_outputs.scheduled_seq_groups,
                scheduler_outputs.ignored_seq_groups, seq_group_metadata_list)
		...
        return request_outputs

self.model_executor.execute_model,调用与vllm耦合后的LLM模型进行推理。这是本篇要讲解内容,我们先来看下模型输入长什么样,

execute_model_req:
在这里插入图片描述
从调度系统中获得,可以用于做推理的seq_groups, 对seq_groups及可用到的各种属性做了封装,暂时不必管都是什么意思,用到时再现场分析。

8.2 如何使用具体模型

在这里插入图片描述

8.1中完成了资源调度工作,接下来该送入初始化好的模型进行推理了。不过vllm对具体模型的又做了多层封装:

8.1中模型调用指向gpu_executor:

  • vllm_module/executor/gpu_executor.py
    def execute_model(
            self, execute_model_req: ExecuteModelRequest
    ) -> Optional[List[Union[SamplerOutput, PoolerOutput]]]:
        output = self.driver_worker.execute_model(execute_model_req)
        return output

self.driver_worker.execute_model指向work_base实例的方法, 这个execute_model方法主要对输入数据进行预处理:

  • vllm/worker/worker_base.py class LocalOrDistributedWorkerBase(WorkerBase)
    def execute_model(
            self,
            execute_model_req: Optional[ExecuteModelRequest] = None
    ) -> Optional[List[SamplerOutput]]:
        """Executes at least one model step on the given sequences, unless no
        sequences are provided."""
        if self.is_driver_worker:
			...
            model_input: ModelRunnerInputBase = (
                self.model_runner.prepare_model_input(
                        execute_model_req.seq_group_metadata_list,
                        execute_model_req.virtual_engine,
                        execute_model_req.finished_requests_ids))
            num_steps = execute_model_req.num_steps
            ...
        else:
			...

        self.execute_worker(worker_input)
		...
        output = self.model_runner.execute_model(
                model_input, self.kv_cache[worker_input.virtual_engine]
                if self.kv_cache is not None else None, intermediate_tensors, num_steps)
		...
        # output is List[SamplerOutput]
        return output

8.2 input_ids预处理与block槽位填充

self.model_runner.prepare_model_input主要功能是合并input,本次共传入3条数据,但在输入模型前,vllm把它们的token全部合在一起了。它们的位置关系通过position区分,这部分代码比较简单,不再贴出了(代码多次跳转后,在vllm_module/worker/model_runner.py def build(…)中完成)

input_ids.shape=[num_tokens, ] 假如输入的3条prompt长度分别为48,44,43,那么num_tokens=135
但是transformers中的推理模式输入shape为[batch_size, num_tokens], vllm 为什么要这样处理呢?

我认为目的是为了避免seq的pad步骤, 因为transformers的推理格式需要对seq做pad,处理为同一长度才能进行batch推理。vllm合并后相当于每个token就是一个batch,不需要再做pad和去pad操作(input_ids做embedding后才做推理,这时的shape为[num_tokens, embed_size],此时num_tokens成为形式上的batchsize)。

input_ids合并后计算结果与transformers是一样的,因为线性变换是逐元素进行的(只是shape有所不同)。

vllm与transformers对输入input的处理方式不同,对应的模型结构也要改变,在第四篇文章,在load_weight中有对hf模型是如何转换到vllm规格的有详细描述。

在这里插入图片描述

在进行推理前,我们还需要把准备prefill的prompt的每个token(就是上面的input_ids, 这时还没做embedding操作)映射到block中,如seq_id=0的prompt长度为48,由于block_size=16, 所以他刚好能填充3个block(编号为2759,2758,2757)。映射关系会写入到slot_mapping列表中,那么这个操作如何来做呢?
在这里插入图片描述

  • vllm/attention/backends/utils.py
    经过多次跳转后(头都绕晕了),槽位填充的核心代码如下,代码比较简单,就是给标记已使用的block对应的槽位,在全局blocks中的索引号。以索引号2759的block来说,它的第一个槽位号是275916=44144,对应着该prompt(或者说decode阶段的1个待输出的token) 的第一个token。,这么做的目的是为以后把计算好的kv值直接填入这些槽位,起到索引作用。
    在这里插入图片描述
    最后slot_mapping填充效果如下面有图所示,第一个prompt有48个token,刚好能填满3个block,那么decode阶段,该seq生成第一个token时就要申请一个新的block了。第三个prompt有43个token,填不满3个block,它的最后一个block使用了11个槽位(43-16
    2),即从44016到44026。
    在这里插入图片描述

8.3 模型推理

8.2节 中self.model_runner.execute_model指向如下代码,这段的代码的主要功能是分配推理模型model_executable,

  • vllm/worker/model_runner.py
    model_input:
    在这里插入图片描述
    def execute_model(
            self,
            model_input: ModelInputForGPUWithSamplingMetadata,
            kv_caches: List[torch.Tensor],
            intermediate_tensors: Optional[IntermediateTensors] = None,
            num_steps: int = 1,
    ) -> Optional[Union[List[SamplerOutput], IntermediateTensors]]:
		...
        # Currently cuda graph is only supported by the decode phase.
        assert model_input.attn_metadata is not None
        prefill_meta = model_input.attn_metadata.prefill_metadata
        decode_meta = model_input.attn_metadata.decode_metadata
        # TODO(andoorve): We can remove this once all
        # virtual engines share the same kv cache.
        virtual_engine = model_input.virtual_engine
        if prefill_meta is None and decode_meta.use_cuda_graph:
            assert model_input.input_tokens is not None
            graph_batch_size = model_input.input_tokens.shape[0]
            model_executable = self.graph_runners[virtual_engine][graph_batch_size]
        else:
            model_executable = self.model
		...
        hidden_or_intermediate_states = model_executable(
                input_ids=model_input.input_tokens,
                positions=model_input.input_positions,
                kv_caches=kv_caches,
                attn_metadata=model_input.attn_metadata,
                intermediate_tensors=intermediate_tensors,
                **MultiModalInputs.as_kwargs(multi_modal_kwargs, device=self.device),
                **seqlen_agnostic_kwargs)
		...

还记得第四篇文章的get_model的操作吗,也是在model_runner.py中完成的,所以这里的self.model之前初始化过的模型。
我们使用第四篇文章用过的llama3.1来剖析剩余代码,model_executable最终执行llama模型的forward代码。

  • vllm/model_executor/models/llama.py class LlamaForCausalLM
    在这里插入图片描述

llama结构类型的大模型的推理,可分为两个阶段:prompt和generate, 在使用kv-cache的情况下,二者的区别仅是输入数据维度的差异,即generate阶段seq序列长度始终为1, 不过在vllm中却有不一样的处理,prefill之后,会把模型构建为cuda计算图,这样计算会更加高效。

经过漫长的准备工作,终于可以开始具体的推理工作,为了这个时刻,整整铺垫了四篇文章!

vllm最终调用的模型推理代码:

  • vllm/model_executor/models/llama.py class LlamaModel

8.31 第一次推理(prefill)
又称为预填充

输入参数:
在这里插入图片描述

    def forward(
        self,
        input_ids: Optional[torch.Tensor],
        positions: torch.Tensor,
        kv_caches: List[torch.Tensor],
        attn_metadata: AttentionMetadata,
        intermediate_tensors: Optional[IntermediateTensors],
        inputs_embeds: Optional[torch.Tensor] = None,
    ) -> Union[torch.Tensor, IntermediateTensors]:
        if get_pp_group().is_first_rank:
            if inputs_embeds is not None:
                hidden_states = inputs_embeds
            else:
            	# 输入的通常都是未embedding的token,在这里进行词嵌入
                hidden_states = self.get_input_embeddings(input_ids)
            residual = None
        else:
            assert intermediate_tensors is not None
            hidden_states = intermediate_tensors["hidden_states"]
            residual = intermediate_tensors["residual"]

        for i in range(self.start_layer, self.end_layer):
            layer = self.layers[i]
            hidden_states, residual = layer(
                positions,	# shape=[num_tokens,]
                hidden_states,	# shape=[num_tokens,embed_size]
                kv_caches[i - self.start_layer],	# 当前layer对应的kv-cache
                attn_metadata,	# 保存着slot_mapping, 通过这个map向kv-cache中填值
                residual,
            )
		...
        return hidden_states

计算模块注意发生在layer层的attention部分:

  • vllm_module/model_executor/models/llama.py class LlamaAttention
    def forward(
        self,
        positions: torch.Tensor,
        hidden_states: torch.Tensor,
        kv_cache: torch.Tensor,
        attn_metadata: AttentionMetadata,
    ) -> torch.Tensor:
        qkv, _ = self.qkv_proj(hidden_states)
        q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
        q, k = self.rotary_emb(positions, q, k)
        attn_output = self.attn(q, k, v, kv_cache, attn_metadata)
        output, _ = self.o_proj(attn_output)
        return output

计算过程中产生变量:
在这里插入图片描述
k,v的shape为[135,1024], q的shape为[135,4096], 说明使用了GQA技术,即4个q共享一个kv

接下来我们看下最重要的self.attn(…)的计算模块:

  • vllm_module/attention/backends/flash_attn.py class FlashAttentionImpl(AttentionImpl)
    def forward(
        self,
        query: torch.Tensor,
        key: torch.Tensor,
        value: torch.Tensor,
        kv_cache: torch.Tensor,
        attn_metadata: FlashAttentionMetadata,
        k_scale: float = 1.0,
        v_scale: float = 1.0,
        attn_type: AttentionType = AttentionType.DECODER,
    ) -> torch.Tensor:
		...
        num_tokens, hidden_size = query.shape
        # Reshape the query, key, and value tensors.
        # query.shape=[135, 32, 128]
        query = query.view(-1, self.num_heads, self.head_size)
        # key.shape=[135, 8, 128]
        key = key.view(-1, self.num_kv_heads, self.head_size)
        # value.shape=[135, 8, 128]
        value = value.view(-1, self.num_kv_heads, self.head_size)

        if kv_cache is not None:
        	# 取出该层缓存key的block,key_cache.shape=[1756, 16, 8, 128]
        	# 关于这个shape的维度含义,再第四篇文章中已经讲过了
            key_cache = kv_cache[0]
            value_cache = kv_cache[1]
			# 调用cuda核函数缓存kv值
            ops.reshape_and_cache_flash(
                key,
                value,
                key_cache,
                value_cache,
                attn_metadata.slot_mapping.flatten(),
                self.kv_cache_dtype,
                k_scale,
                v_scale,
            )
		...
        if prefill_meta := attn_metadata.prefill_metadata:
            # Prompt run.
            if (kv_cache is None or prefill_meta.block_tables is None
                    or prefill_meta.block_tables.numel() == 0):
				# 计算attention值
                out = flash_attn_varlen_func(
                    q=query,
                    k=key,
                    v=value,
                    cu_seqlens_q=prefill_meta.seq_start_loc,
                    cu_seqlens_k=prefill_meta.seq_start_loc,
                    max_seqlen_q=prefill_meta.max_prefill_seq_len,
                    max_seqlen_k=prefill_meta.max_prefill_seq_len,
                    softmax_scale=self.scale,
                    causal=True,
                    window_size=self.sliding_window,
                    alibi_slopes=self.alibi_slopes,
                    softcap=self.logits_soft_cap,
                )
                assert output[:num_prefill_tokens].shape == out.shape
                output[:num_prefill_tokens] = out
            else:
				...

        # Reshape the output tensor.
        return output.view(num_tokens, hidden_size)

该模块主要完成两个功能:缓存kv值和计算attention。

保存kv-cache的操作发生在ops.reshape_and_cache_flash(…)中:

  • vllm/_custom_ops.py
def reshape_and_cache_flash(
    key: torch.Tensor,
    value: torch.Tensor,
    key_cache: torch.Tensor,
    value_cache: torch.Tensor,
    slot_mapping: torch.Tensor,
    kv_cache_dtype: str,
    k_scale: float,
    v_scale: float,
) -> None:
    torch.ops._C_cache_ops.reshape_and_cache_flash(key, value, key_cache,
                                                   value_cache, slot_mapping,
                                                   kv_cache_dtype, k_scale,
                                                   v_scale)

很可惜,torch.ops._C_cache_ops.reshape_and_cache_flash已经被打包到.so文件中,不能断点调试。这是用CUDA实现的核函数,我们可以找到编译前的源码:

  • csrc/cache_kernels.cu
void reshape_and_cache_flash(
    torch::Tensor& key,        // [num_tokens, num_heads, head_size]
    torch::Tensor& value,      // [num_tokens, num_heads, head_size]
    torch::Tensor& key_cache,  // [num_blocks, block_size, num_heads, head_size]
    torch::Tensor&
        value_cache,  // [num_blocks, block_size, num_heads, head_size]
    torch::Tensor& slot_mapping,  // [num_tokens]
    const std::string& kv_cache_dtype, const double k_scale,
    const double v_scale) {
	...
  TORCH_CHECK(key_cache.stride(0) == value_cache.stride(0));

  dim3 grid(num_tokens);
  dim3 block(std::min(num_heads * head_size, 512));
  const at::cuda::OptionalCUDAGuard device_guard(device_of(key));
  const cudaStream_t stream = at::cuda::getCurrentCUDAStream();

  DISPATCH_BY_KV_CACHE_DTYPE(key.dtype(), kv_cache_dtype,
                             CALL_RESHAPE_AND_CACHE_FLASH);
}

这是个数据预处理函数,真正工作的是被CALL_RESHAPE_AND_CACHE_FLASH宏定义的函数

#define CALL_RESHAPE_AND_CACHE_FLASH(KV_T, CACHE_T, KV_DTYPE)         \
  vllm::reshape_and_cache_flash_kernel<KV_T, CACHE_T, KV_DTYPE>       \
      <<<grid, block, 0, stream>>>(                                   \
          reinterpret_cast<KV_T*>(key.data_ptr()),                    \
          reinterpret_cast<KV_T*>(value.data_ptr()),                  \
          reinterpret_cast<CACHE_T*>(key_cache.data_ptr()),           \
          reinterpret_cast<CACHE_T*>(value_cache.data_ptr()),         \
          slot_mapping.data_ptr<int64_t>(), block_stride, key_stride, \
          value_stride, num_heads, head_size, block_size, k_scale, v_scale);
__global__ void reshape_and_cache_flash_kernel(
    const scalar_t* __restrict__ key,    // [num_tokens, num_heads, head_size]
    const scalar_t* __restrict__ value,  // [num_tokens, num_heads, head_size]
    cache_t* __restrict__ key_cache,     // [num_blocks, block_size, num_heads,
                                         // head_size]
    cache_t* __restrict__ value_cache,   // [num_blocks, block_size, num_heads,
                                         // head_size]
    const int64_t* __restrict__ slot_mapping,  // [num_tokens]
    const int block_stride, const int key_stride, const int value_stride,
    const int num_heads, const int head_size, const int block_size,
    const float k_scale, const float v_scale) {
  // 每个cuda block处理一个token
  const int64_t token_idx = blockIdx.x;
  const int64_t slot_idx = slot_mapping[token_idx];
  
  // 如果槽索引小于 0,表示 token 被填充(padding),则直接返回
  if (slot_idx < 0) {
    return;
  }
   // 计算 block 索引和 block 内的偏移量
  const int64_t block_idx = slot_idx / block_size;
  const int64_t block_offset = slot_idx % block_size;
  
  // 计算每个注意力头和每个头的总数据量
  const int n = num_heads * head_size;
  
  // 每个线程处理数据中的一个元素
  for (int i = threadIdx.x; i < n; i += blockDim.x) {
    // 计算当前线程处理的 key 和 value 数据在输入数组中的索引
    const int64_t src_key_idx = token_idx * key_stride + i;
    const int64_t src_value_idx = token_idx * value_stride + i;
    // 计算当前元素对应的注意力头索引和头内的偏移量
    const int head_idx = i / head_size;
    const int head_offset = i % head_size;
    // 计算在缓存中目标位置的索引
    const int64_t tgt_key_value_idx = block_idx * block_stride +
                                      block_offset * num_heads * head_size +
                                      head_idx * head_size + head_offset;
    
    // 从输入数组中加载当前的 key 和 value 数据
    scalar_t tgt_key = key[src_key_idx];
    scalar_t tgt_value = value[src_value_idx];
    
    // 缓存kv值
    // 如果使用自动类型,不进行额外的缩放和转换,直接存储
    if constexpr (kv_dt == Fp8KVCacheDataType::kAuto) {
      key_cache[tgt_key_value_idx] = tgt_key;
      value_cache[tgt_key_value_idx] = tgt_value;
    } else {	// 否则,使用指定的缩放因子对数据进行转换后存储
      key_cache[tgt_key_value_idx] =
          fp8::scaled_convert<cache_t, scalar_t, kv_dt>(tgt_key, k_scale);
      value_cache[tgt_key_value_idx] =
          fp8::scaled_convert<cache_t, scalar_t, kv_dt>(tgt_value, v_scale);
    }
  }
}

通过写的reshape_and_cache_flash_kernel的注释已经清楚看到pagedAttention缓存kv的真实过程。

关于attention的计算,经过多次跳转好,由如下代码实现:

  • /usr/local/miniconda3/lib/python3.11/site-packages/vllm_flash_attn/flash_attn_interface.py
def _flash_attn_varlen_forward(q,  k, v, cu_seqlens_q,cu_seqlens_k,  max_seqlen_q,  max_seqlen_k,   dropout_p,
    softmax_scale,  causal,  window_size,  softcap,   alibi_slopes,   return_softmax, block_table,
    *, out=None
):
    q, k, v = [maybe_contiguous(x) for x in (q, k, v)]
    out, q, k, v, out_padded, softmax_lse, S_dmask, rng_state = flash_attn_cuda.varlen_fwd( q, k,  v,  out, 
                 cu_seqlens_q, cu_seqlens_k, None,    block_table,alibi_slopes, max_seqlen_q, max_seqlen_k,
                 dropout_p,   softmax_scale,   False,   causal,    window_size[0],    window_size[1],   softcap,
                 return_softmax,     None,
    )
    # if out.isnan().any() or softmax_lse.isnan().any():
    #     breakpoint()
    return out, q, k, v, out_padded, softmax_lse, S_dmask, rng_state

flash_attn_cuda函数来自. so包, 没找到源码!

8.32 非第一次推理(decode阶段)

经过预填充阶段后,vllm会把模型本身及推理过程处理成cuda计算图,正式的解码阶段,会直接使用计算图获得推理结果。

对应8.3开始代码中的model_executable 选择分支:
在这里插入图片描述

在decode推理前,我们先来看下输入参数与prefill有什么不同:
在初始阶段我们设定每个seq生成4条output,关于拼接原理,在第一篇文章由详细讲过了。
从model_input数据结构看,此时的模型输入只有一个token(这是prefill后生成的第一个token)。

在这里插入图片描述
看上图中input_tokens,有没有发现什么奇怪的事?

我们输入的prompt数量为3,设定每个prompt生成4条output,为什么这里是16个token? 这是因为decode使用的是cuda计算图,图需要固定大小的张量,这部分细节不想在此深究了~,有兴趣的自行探索吧。

计算图执行的推理流程如下:

  • vllm/worker/model_runner.py class CUDAGraphRunner
def forward(
        self,
        input_ids: torch.Tensor,                       # 输入的 token IDs 张量
        positions: torch.Tensor,                       # 输入的位置信息张量
        kv_caches: List[torch.Tensor],                 # KV cache 列表(这里被删除,不再使用)
        attn_metadata: AttentionMetadata,              # 注意力元数据,包含 slot_mapping 和其他解码元数据
        intermediate_tensors: Optional[IntermediateTensors],  # 中间张量,可能包含中间结果的数据
        **kwargs,                                      # 其他关键字参数,用于额外的自定义操作
) -> torch.Tensor:
    # KV caches 是固定的张量,因此在后续操作中不需要复制它们
    del kv_caches  # 删除 kv_caches,因为它们不再需要

    # 将输入张量复制到模型的输入缓冲区
    self.input_buffers["input_ids"].copy_(input_ids, non_blocking=True)  # 复制输入 token IDs
    self.input_buffers["positions"].copy_(positions, non_blocking=True)  # 复制位置信息
    self.input_buffers["slot_mapping"].copy_(attn_metadata.slot_mapping, non_blocking=True)  # 复制 slot_mapping
    
    # 根据后端的不同,处理额外的输入数据
    if self.backend_name != "flashinfer":
        # 如果后端不是 "flashinfer",复制解码元数据中的序列长度和块表
        self.input_buffers["seq_lens_tensor"].copy_(
                attn_metadata.decode_metadata.seq_lens_tensor,
                non_blocking=True)
        self.input_buffers["block_tables"].copy_(attn_metadata.decode_metadata.block_tables, non_blocking=True)
    
    # 如果 input_buffers 包含 "seqlen_agnostic_capture_inputs",在 CUDA 图之前复制输入
    if "seqlen_agnostic_capture_inputs" in self.input_buffers:
        self.model.copy_inputs_before_cuda_graphs(self.input_buffers, **kwargs)

    # 如果提供了 intermediate_tensors,复制这些中间张量到输入缓冲区
    if intermediate_tensors is not None:
        for key in intermediate_tensors.tensors:
            self.input_buffers[key].copy_(intermediate_tensors[key], non_blocking=True)
    
    # 执行计算图,计算存储在self的各个属性中
    # 这个计算图是核心代码,可惜这里看不到。
    self.graph.replay()
    
    # 如果 input_buffers 包含 "seqlen_agnostic_capture_inputs",在 CUDA 图之后复制输出
    if "seqlen_agnostic_capture_inputs" in self.input_buffers:
        self.model.copy_outputs_after_cuda_graphs(self.input_buffers, **kwargs)
    
    # 返回输出张量
    if get_pp_group().is_last_rank:
        return self.output_buffers["hidden_states"]  # 如果是最后一个进程,返回隐藏状态张量

    return self.output_buffers  # 否则返回输出缓冲区

后记

本篇文章仅梳理了vllm大致的模型推理流程,省去了很多代码细节;即使如此,这仍是一个极其复杂的耦合过程。在写作本篇文章时,官网又把vllm更新到了0.6.0,与0.5.4做了比较,又有很多改动。这个系列的文章还没写完,就要过时了???

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

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

相关文章

基于机器学习的电商优惠券核销预测

1. 项目简介 随着移动互联网的快速发展&#xff0c;O2O&#xff08;Online to Offline&#xff09;模式已成为电商领域的一大亮点。优惠券作为一种有效的营销工具&#xff0c;被广泛应用于吸引新客户和激活老用户。然而&#xff0c;传统的随机投放方式往往效率低下&#xff0c;…

JavaWeb【day11】--(SpringBootWeb案例)

SpringBootWeb案例 前面我们已经实现了员工信息的条件分页查询以及删除操作。 关于员工管理的功能&#xff0c;还有两个需要实现&#xff1a; 新增员工 修改员工 首先我们先完成"新增员工"的功能开发&#xff0c;再完成"修改员工"的功能开发。而在&quo…

万能视频下载器-下载所有网站上的任何视频

万能视频下载器-下载所有网站上的任何视频 在Edge浏览器中发现了一款令人惊叹的视频下载扩展插件&#xff0c;简直就是视觉盛宴的利器&#xff01;只需轻点几下&#xff0c;在拓展商店中轻松查找并安装&#xff0c;你便能随时随地随心所欲地把心仪的视频收入囊中。无论是教学资…

matlab仿真 OFDM系统仿真

&#xff08;内容源自详解MATLAB&#xff0f;SIMULINK 通信系统建模与仿真 刘学勇编著第九章内容&#xff0c;有兴趣的读者请阅读原书&#xff09; clear all N8;%子载波数 f1:N;%各个子载波频率 xrandi([0 3],1,N);%子载波上的数据 x1qammod(x,4);%4-QAM调制 t0:0.001:1-0.…

学习周报-2024.9.3

目录 摘要 Abstract 文献阅读&#xff1a;一种改善河流水质预测的耦合模型以解决非平稳性和数据限制 一、现有问题 二、提出方法 三、相关知识 1、基于小波分析的LSTM&#xff08;WA-LSTM&#xff09; 2、迁移学习TL改进WA-LSTM 四、WA-LSTM-TL模型 五、研究实验 1、…

手写NACOS的服务的注册与发现|心跳机制|轮询调用服务功能

背景 手写NACOS的服务的部分核心功能&#xff0c;提高自身的编码能力 本篇文章设计的是单体NACOS后端服务&#xff0c;提供SDK给多个NACOS客户端使用 本文编写了注册与发现|心跳机制|轮询调用服务功能&#xff0c;可当做入门级阅读 nacos-service 项目结构 代码内容 pom配置…

Detect It Easy

Detect It Easy&#xff08;简称 DIE&#xff09;项目的网址为 https://github.com/horsicq/Detect-It-Easy 下载完安装包后&#xff0c;直接双击die.exe即可进入到操作界面 工具介绍&#xff1a; 它可以用来检测程序架构和文件类型。如图所示。其中&#xff0c;「模式」说明程…

UE5 贝塞尔曲线导弹

首先创建导弹Actor蓝图 代码逻辑&#xff0c;这其中创建的所有变量都不用添加值&#xff0c;这些逻辑要画图来解释&#xff0c;比较麻烦&#xff0c;大家自行理解一下 接下来进入人物蓝图编写代码逻辑&#xff0c;我这里是在两个不同的位置发射两枚导弹 宏中的代码&#xff0c;…

时序预测|基于粒子群优化支持向量机的时间序列预测Matlab程序PSO-SVM 单变量和多变量 含基础模型

时序预测|基于粒子群优化支持向量机的时间序列预测Matlab程序PSO-SVM 单变量和多变量 含基础模型 文章目录 一、基本原理1. 问题定义2. 数据准备3. SVM 模型构建4. 粒子群优化&#xff08;PSO&#xff09;5. 优化与模型训练6. 模型评估与预测7. 流程总结8. MATLAB 实现概述 二、…

Python QT实现A-star寻路算法

目录 1、界面使用方法 2、注意事项 3、补充说明 用Qt5搭建一个图形化测试寻路算法的测试环境。 1、界面使用方法 设定起点&#xff1a; 鼠标左键双击&#xff0c;设定红色的起点。左键双击设定起点&#xff0c;用红色标记。 设定终点&#xff1a; 鼠标右键双击&#xf…

轻松上手,高效产出:音频剪辑工具年度精选

不知道你有没有拍vlog记录生活的习惯&#xff0c;有时候视频里穿插进自己的声音能让视频更加丰富贴上自己的标签。这次我们一起探讨当下有哪些好用的在线音频剪辑工具。 1.FOXIT音频剪辑 链接直达>>https://www.foxitsoftware.cn/audio-clip/ 这个工具是一款专业的音…

GNU的伪操作 (25)

这里主要是 对 GNU的 各个伪操作进行 详细的解释。 先来看着几个 伪操作。 .byte, .short, .long, .quad , .float , 这个是关于 字节的。 .string .ascii 是关于字符串的。 这个字符串编译器是可以自动在末尾补0 的。 举例&#xff1a; val: .word 0x11223344 mov r…

计算机组成原理(SRAM电路图示)

1.该电路由6个MOS管&#xff08;T1-T6&#xff09;组成 2.T1-T4是一个由MOS管组成的触发器基本电路&#xff1b; T5&#xff0c;T6像开关&#xff0c;受行地址选择信号控制&#xff1b; T7,T8受列地址选择控制&#xff0c;分别与位线A&#xff0c;和相连 3.假设触发器…

FinOps原则:云计算成本管理的关键

导语&#xff1a; FinOps 原则为我们提供了北极星&#xff08;North Star&#xff09;&#xff0c;在我们实践云财务管理时指导我们的活动。这些原则由 FinOps 基金会成员制定&#xff0c;并通过经验磨练出来。 北极星&#xff08;North Star&#xff09;的含义&#xff1a; …

不用管理员权限直接修改windows中hosts值的方法

本文只适用于少数经常修改hosts文件的程序员帅哥和美女们。 背景&#xff1a;直接修改hosts文件的不足 修改C:\Windows\System32\drivers\etc\hosts时&#xff0c;必须要管理员权限。 经常修改&#xff0c;会觉得有一丝丝麻烦。 方法1 &#xff08;安全性低&#xff0c;不…

ThinkPHP5 5-rce远程代码执行漏洞复现

启动容器 docker-compose up -d 查看端口 docker ps 端口为:8080,访问网站&#xff0c;搭建成功 漏洞复现 &#xff08;1&#xff09;输出关于 PHP 配置的信息 &#xff08;2&#xff09;将php代码写入文件 接着访问shell.php 由于存在过滤&#xff0c;需要用到base64加密来使…

SPIRNGBOOT+VUE实现浏览器播放音频流并合成音频

一、语音合成支持流式返回&#xff0c;通过WS可以实时拿到音频流&#xff0c;那么我们如何在VUE项目中实现合成功能呢。语音合成应用非常广泛&#xff0c;如商家广告合成、驾校声音合成、新闻播报、在线听书等等场景都会用到语音合成。 二、VUE下实现合成并使用浏览器播放代码…

学习记录:js算法(二十八):删除排序链表中的重复元素、删除排序链表中的重复元素II

文章目录 删除排序链表中的重复元素我的思路解法一&#xff1a;循环解法二&#xff1a;递归 网上思路 删除排序链表中的重复元素 II我的思路网上思路 总结 删除排序链表中的重复元素 给定一个已排序的链表的头 head &#xff0c; 删除所有重复的元素&#xff0c;使每个元素只出…

软件质量保障:故障演练介绍

目录 背景&#xff1a;架构变化带来的问题 什么是故障演练 为什么需要故障演练 故障演练场景有哪些 不同演练类型和目标 如何对工具进行评估 功能评测项 告警评测项 观测指标评测项 总结 背景&#xff1a;架构变化带来的问题 随着架构越来越复杂、应用越来越多样&…

外卖霸王餐对接接口为用户提供了哪些好处?

外卖霸王餐对接接口为用户提供了多种好处&#xff0c;以下是一些主要优势&#xff1a; 免费或低成本的美食体验&#xff1a;用户可以通过霸王餐活动免费或以非常低的价格尝试不同的餐厅和菜品。发现新餐厅和菜品&#xff1a;霸王餐活动可以帮助用户发现新的餐厅和他们可能感兴趣…