CUDA-MODE课程笔记 第9课: 归约(也对应PMPP的第10章)

2024-09-03 19:44

本文主要是介绍CUDA-MODE课程笔记 第9课: 归约(也对应PMPP的第10章),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

我的课程笔记,欢迎关注:https://github.com/BBuf/how-to-optim-algorithm-in-cuda/tree/master/cuda-mode

CUDA-MODE课程笔记 第9课: 归约(也对应PMPP的第10章)

课程笔记

本节课的题目。

这节课的内容主要是 Chapter 10 of PMPP book ,Slides里面还给出了本节课的代码所在位置以及如何编译以及使用nsight compute进行profile。

这张slides给出了reduction(归约)操作的定义,reduction(归约)是减少输出大小的操作。最典型的reduction操作是将向量(vector)转换为标量(scalar)。然后还列举了一些常见的reduction操作,包括min(最小值),max(最大值),argmax(最大值的索引),argmin(最小值的索引),norm(范数),sum(求和),prod(乘积),mean(平均值),unique(唯一值)。https://github.com/cuda-mode/lectures/blob/main/lecture_009/torch_reductions.py 这个文件展示了一些 reduction 操作的例子。

def reduce(data, identity, op):result = identityfor element in data:result = op(result, element)return result# Example usage:# Summation
data = [1, 2, 3, 4, 5]
print(reduce(data, 0, lambda a, b: a + b))  # Output: 15# Product
print(reduce(data, 1, lambda a, b: a * b))  # Output: 120# Maximum
print(reduce(data, float('-inf'), max))  # Output: 5# Minimum
print(reduce(data, float('inf'), min))  # Output: 1

在PyTorch中有一个通用的Recuce算子来做所有的归约操作,所以你不能看到reduce_max这种单独的算子。

这张Slides强调了归约操作在深度学习和机器学习中的普遍性,例如:

  • Mean/Max pooling(平均/最大池化):这是在卷积神经网络中常用的操作,用于减少特征图的空间尺寸,提取主要特征。
  • Classification: Argmax(分类:最大值索引):在分类任务中,通常使用argmax来确定最可能的类别。
  • Loss calculations(损失计算):在训练过程中,通常需要计算损失函数,这往往涉及到对多个样本损失的归约操作。
  • Softmax normalization(Softmax归一化):在多分类问题中,Softmax用于将原始的输出分数转换为概率分布,这个过程也涉及归约操作。

这张Slides展示了一个PyTorch使用归约操作(在这个case中是求最大值)来处理张量数据的例子。我们可以看到torch.max这个算子的实现是在 https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/cuda/ReduceOps.cpp 这里,并不是一个单独的kernel实现,而是注册为通用的Reduce Op里面的其中一个。

这张Slides展示了一个"串行归约"(Serial reduction)的例子,具体是关于最大值(Max)操作的过程。

  • 处理方法如下:
    • “Go through elements 1 by 1”(逐个遍历元素)
    • “Compare new number to old max if greater then update”(将新数字与当前最大值比较,如果更大则更新)
  • 右侧图示展示了具体的迭代过程:
    • 初始向量(Initial Vector)是 [5, 2, 8, 1]
    • 迭代1:[5](第一个元素作为初始最大值)
    • 迭代2:[5, 5](2小于5,最大值保持不变)
    • 迭代3:[5, 5, 8](8大于5,更新最大值)
    • 迭代4:[5, 5, 8, 8](1小于8,最大值保持不变)

这张Slides主要展示了数据处理中的两种策略:转换(Transformation)和归约(Reduction),以及它们各自的线程策略。继续看归约是怎么做的。

  • 这张Slides展示了并行归约(Parallel Reduction)的可视化过程,主要讲解了如何通过并行计算找出一个向量中的最大值。
  • 算法步骤:
    • 每一步取一对元素,计算它们的最大值,并将新的最大值存储在新向量中。
    • 重复这个过程,直到向量中只剩下1个元素。
    • 整个过程需要 O(log n) 步完成,其中 n 是初始向量的元素数量。
  • Slides右边的图展示了具体的归约过程:
    • 初始向量:[5, 2, 8, 1, 9, 3, 7, 4, 6, 0]
    • 第1步归约:[5, 8, 9, 7, 6]
    • 第2步归约:[8, 9, 7]
    • 第3步归约:[9, 9]
    • 最终步骤:[9]
      这个算法是后续cuda kernel实现的基础。

这张Slides以树的方式可视化了上面的并行Reduction算法,只不过这里是求和而不是求最大值了。这里需要注意的是,浮点数加法不满足交换律,即A=B不等于B+A,使用PyTorch时这一点经常引起混淆。在这个例子中,我们无法控制GPU线程执行的先后顺序,所以无法控制合并两个元素的顺序,这也是不确定性的源头之一。

PyTorch中使用torch.use_deterministic_algorithms(True)来控制使用确定性的算法,但是这种算法一般会降低运行速度。https://github.com/cuda-mode/lectures/blob/main/lecture_009/nondeterminism.py 这个文件举了一个例子说明由于浮点数精度问题导致求和结果的不确定性的问题。

# We'll use several small numbers that, when added together first, could show a difference
numbers = [1e-20] * 10 + [1e20, -1e20]  # 10 small numbers followed by a large positive and negative number# Sum the list from left to right
sum_left_to_right_adjusted = sum(numbers)# Sum the list from right to left
sum_right_to_left_adjusted = sum(reversed(numbers))# 0.0 9.999999999999997e-20
print(sum_left_to_right_adjusted, sum_right_to_left_adjusted)

另外想说明的问题是在《CUDA-MODE课程笔记 第7课: Quantization Cuda vs Triton》讲到过即使是进行INT4/INT8量化时,累加运算往往在更高的精度下运行,其原因在于如果你在float16中累加许多小的数值,最后可能会出现大数吃小数的情况。解决方法有两种:要么使用bf16这种具有更高动态范围的数据类型,要么在float32高精度上进行累加。例如当我们查看Triton矩阵乘法的教程时,我们会发现他的累加器一般都是float32。这个例子的代码为:https://github.com/cuda-mode/lectures/blob/main/lecture_009/accuracy.py

import torch
large_value = torch.tensor([1000.0], dtype=torch.float32)  # Using float32 for initial value# Define a smaller value that is significant for float32 but not for float16
small_value = torch.tensor([1e-3], dtype=torch.float32)  # Small value in float32# Add small value to large value in float32
result_float32 = large_value + small_value# Convert large value to float16 and add the small value (also converted to float16)
result_float16 = large_value.to(torch.float16) + small_value.to(torch.float16)# Convert results back to float32 for accurate comparison
result_float32 = result_float32.item()
result_float16_converted = result_float16.to(torch.float32).item()# Print results
# 1000.0009765625 1000.0
print(result_float32, result_float16_converted)

这张Slides建议结合 https://github.com/cuda-mode/lectures/blob/main/lecture_009/simple_reduce.cu 实现代码来看:

__global__ void SimpleSumReductionKernel(float* input, float* output) {unsigned int i = 2 * threadIdx.x;for (unsigned int stride = 1; stride <= blockDim.x; stride *= 2) {if (threadIdx.x % stride == 0) {input[i] += input[i + stride];}__syncthreads();}if (threadIdx.x == 0) {*output = input[0];}
}

对于SimpleSumReductionKernel来说,每个线程负责处理两个相邻的元素,这就是为什么Slides中显示8个线程处理16个元素。然后for 循环实现了归约过程,对应图中的每一层。stride 从1开始,每次迭代翻倍,这解释了为什么每次迭代活跃线程数减半。

  • 我们可以模拟一下归约的过程:
    • 第一次迭代 (stride = 1): 每个线程将相邻的两个元素相加。
    • 第二次迭代 (stride = 2): 每隔一个线程进行计算,将间隔为2的元素相加。
    • 以此类推,直到最后只有一个线程(线程0)进行最后的加法。
  • 此外,cuda代码中 __syncthreads() 确保每次迭代后所有线程同步。
  • 代码中的 if (threadIdx.x % stride == 0) 条件导致了Slides中提到的线程不活跃问题。
  • CUDA中,线程以32个为一组(称为warp)执行。由于这种归约方式,很快就会有整个warp变得不活跃,这就是Slides提到的 “A lot of warps will be inactive” 的原因。
  • kernel启动设置:SimpleSumReductionKernel<<<1, size / 2>>>(d_input, d_output); 启动了 size/2 个线程,对应图中的8个线程(假设 size 为16)。
  • Slides建议使用 “ncu -set full” 进行性能分析,这可能会揭示更多关于线程和warp效率的详细信息。

这个版本的代码在T4 GPU上的分支效率为74%。

当我们尝试对上面的kernel进行优化时,我们要牢记在Lecture 8里讲到的CUDA性能检查清单。我们的优化将涉及到Control divergence,Memory divergence,最小化全局内存访问,线程粗化等等。

这张Slides对应的代码是:https://github.com/cuda-mode/lectures/blob/main/lecture_009/control_divergence_reduce.cu ,我们要结合代码中的kernel实现来看:

__global__ void FixDivergenceKernel(float* input, float* output) {unsigned int i = threadIdx.x; //threads start next to each otherfor (unsigned int stride = blockDim.x; stride >= 1; stride /= 2) { // furthest element is blockDim awayif (threadIdx.x < stride) { // input[i] += input[i + stride]; // each thread adds a distant element to its assigned position}__syncthreads();}if (threadIdx.x == 0) {*output = input[0];}
}

Slides和对应的CUDA kernel代码都在讲解如何减少控制分歧(control divergence)并提高并行计算效率。

  • Slides的关键点: “Ensure threads and their owned positions remain close together as time progresses” (确保线程及其拥有的位置随时间推移保持接近)。这正是kernel实现的核心思想。
  • Slides图中展示了多个线程(Thread0到Thread7)如何协同工作。每一行代表一个时间步,蓝色方块表示活跃的线程。随着时间推移,活跃线程数量减少,但它们保持紧密相邻。
  • 操作的实现方式为,步长从blockDim.x开始,并且在每次迭代中,我们把步长除以2,而不是以前那样乘以2,因此直觉上我们希望Stride随着时间推移逐渐减小,这样线程在内存中被合并处理的可能性大大增加。
  • kernel的启动方式和之前的原始实现保持一致。

这里还解释了一下Slides图中的树状迭代的逻辑实际上是因为每次迭代之后的__syncthreads()来完成的。

从ncu的结果可以看到,相比于初始版本,这里的分支效率为99%,相比于初始的74%,这种优化确实是有效的。

此外,ncu的结果还显示当前kernel的L1 Cache命中率为66.88%,这个和下一个shared memory优化相关。

这张Slides对应的代码是:https://github.com/cuda-mode/lectures/blob/main/lecture_009/shared_reduce.cu , kernel实现为:

#define BLOCK_DIM 1024// This is the code from the book but I couldn't get this to run faster even with occupancy calculator
// L1 throughput is dramatically increased though
__global__ void SharedMemoryReduction(float* input, float* output) {__shared__ float input_s[BLOCK_DIM];unsigned int t = threadIdx.x;input_s[t] = input[t] + input[t  + BLOCK_DIM];for (unsigned int stride = blockDim.x/2; stride >= 1; stride /=2) {__syncthreads();if (threadIdx.x < stride) {input_s[t] += input_s[t + stride];}}if (threadIdx.x == 0) {*output = input_s[0];}
}

这张Slides和对应的CUDA kernel代码主要讲解如何通过使用共享内存来最小化全局内存访问,从而提高性能。

  • Slides的关键点:
    • “Initial load from global memory”(初始从全局内存加载)
    • “Subsequent writes and reads continue in shared memory”(后续的读写在共享内存中进行)
  • Slides的图中展示了多个线程(Thread 0到Thread 7)如何协同工作。蓝色方块代表全局内存,绿色方块代表共享内存。箭头表示数据移动和计算过程。
  • 代码解析:
    • __shared__ float input_s[BLOCK_DIM]; 声明了一个共享内存数组。
    • input_s[t] = input[t] + input[t + BLOCK_DIM]; 从全局内存加载数据到共享内存,并进行初步计算。这对应Slides中的"Initial load from global memory"。
    • __syncthreads(); 确保所有线程在进入下一迭代前完成当前操作。

从ncu的结果来看,L1 Cache命中率提升到了60%。不过尽管L1缓存吞吐量显著提高,但实际运行速度可能没有明显提升。这可能是因为其他因素(如全局内存带宽、线程同步开销等)成为了新的瓶颈。

尝试增加这个kernel的输入数据量的时候我们发现kernel结果会出错,这是因为kernel里面的shared memory的大小限制为了1024。在GPU上一般不会做规模着么小的任务。

这张Slides实际上就是说在GPU上使用多个Block来分段处理数据,上面的2个版本的程序都只启动了一个Block,如下图所示:

这张Slides展示的做法就是启动多个Block,然后只要每个单独的Block可以容纳1024分元素,我们就可以在不同的Block中单独进行归约操作,最后再对所有的Block进行一次最后的归约。

这里对应的代码实现为:https://github.com/cuda-mode/lectures/blob/main/lecture_009/segment_reduce.cu ,贴一下这个代码:

#include <iostream>
#include <cuda.h>#define BLOCK_DIM 1024__global__ void SharedMemoryReduction(float* input, float* output, int n) {__shared__ float input_s[BLOCK_DIM]; unsigned int idx = blockIdx.x * blockDim.x + threadIdx.x; // index within a blockunsigned int t = threadIdx.x; // global index// Load elements into shared memoryif (idx < n) {input_s[t] = input[idx];} else {input_s[t] = 0.0f;}__syncthreads();// Reduction in shared memoryfor (unsigned int stride = blockDim.x / 2; stride > 0; stride >>= 1) {if (t < stride && idx + stride < n) {input_s[t] += input_s[t + stride];}__syncthreads();}// Reduction across blocks in global memory// needs to be atomic to avoid contentionif (t == 0) {atomicAdd(output, input_s[0]);}
}int main() {// Size of the input dataconst int size = 100000;const int bytes = size * sizeof(float);// Allocate memory for input and output on hostfloat* h_input = new float[size];float* h_output = new float;// Initialize input data on hostfor (int i = 0; i < size; i++) {h_input[i] = 1.0f; // Example: Initialize all elements to 1}// Allocate memory for input and output on devicefloat* d_input;float* d_output;cudaMalloc(&d_input, bytes);cudaMalloc(&d_output, sizeof(float));// Copy data from host to devicefloat zero = 0.0f;cudaMemcpy(d_output, &zero, sizeof(float), cudaMemcpyHostToDevice);cudaMemcpy(d_input, h_input, bytes, cudaMemcpyHostToDevice);// Launch the kernelint numBlocks = (size + BLOCK_DIM - 1) / BLOCK_DIM;SharedMemoryReduction<<<numBlocks, BLOCK_DIM>>>(d_input, d_output, size);// Copy result back to hostcudaMemcpy(h_output, d_output, sizeof(float), cudaMemcpyDeviceToHost);// Print the resultstd::cout << "Sum is " << *h_output << std::endl;// Cleanupdelete[] h_input;delete h_output;cudaFree(d_input);cudaFree(d_output);return 0;
}

代码中需要特别注意的是kernel的最后一句代码,这是当我们把所有的Block处理完之后,我们进行最后一层的归约操作。我们在Block层面做归约是跨global memory的,这个时候需要使用atomicAdd来避免多个Block在同一个位置写的时候出现竞争错误。

再介绍一种优化策略-线程粗化。我们之前的策略是确保每个线程基本上每次只累加2个元素,可以想一下如果每个线程累加4个或者8个元素会怎么样呢?这个优化对应的代码在 https://github.com/cuda-mode/lectures/blob/main/lecture_009/reduce_coarsening.cu ,kernel实现为:

#define BLOCK_DIM 1024
#define COARSE_FACTOR 2__global__ void CoarsenedReduction(float* input, float* output, int size) {__shared__ float input_s[BLOCK_DIM];unsigned int i = blockIdx.x * blockDim.x * COARSE_FACTOR + threadIdx.x;unsigned int t = threadIdx.x;float sum = 0.0f;// Reduce within a threadfor (unsigned int tile = 0; tile < COARSE_FACTOR; ++tile) {unsigned int index = i + tile * blockDim.x;if (index < size) {sum += input[index];}}input_s[t] = sum;__syncthreads();//Reduce within a blockfor (unsigned int stride = blockDim.x / 2; stride > 0; stride >>= 1) {if (t < stride) {input_s[t] += input_s[t + stride];}__syncthreads();}//Reduce over blocksif (t == 0) {atomicAdd(output, input_s[0]);}
}

COARSE_FACTOR这个参数控制一个线程在一个iter里面累加多少次元素,一次就是累加2个元素,2次就是累加4个元素。这个kernel和上一个分段归约比较类似,不过现在的第一次归约是在单个线程内部进行归约,一旦一个线程上完成了归约,接下来就需要在一个线程块内进行归约,最后是在Block间进行最后的归约。

  • 这张Slides标题为"Next steps"(下一步),主要内容包括:
  • 回顾和建议:
    • 讲座1-8已经提供了开始编写、分析和部署PyTorch内核所需的全部内容。
    • 建议学生开始选择一个项目。
    • 鼓励在#general频道寻找合作者以保持动力。
  • 下一位讲师介绍:
    • 下一位讲师是Oscar。
    • Oscar将讲解如何部署生产级CUDA库。
  • 寻找讲师:
    • 正在寻找有兴趣讲解前缀和(scan)和NCCL的讲师。

作者还额外准备了几张Slides让大家了解一下深度学习框架中归约操作是如何实现的。

例如,以PyTorch为例子,已经有了一系列面向用户的归约操作,比如torch.max/torch.min,torch.mean等等。这些操作是如何用CUDA Kernel实现的呢?我们从上面最后两个版本的优化可以注意到,这些优化考虑是为了输入数据很大的时候做的,但如果输入数据很小,那么上面的所有考虑都变得没有意义了,使用非分段的Reduction算法更合理。对多个维度的数据进行归约应该怎么做?当输入和输出的数据类型发生变化时,我们需要的实现吗?我们是否应该考虑修改累加器的dtype呢?因此,如果你尝试写一个广泛适用的kernel,你需要考虑很多因素。因为如果你构建的kernel仅仅适用于特定的场景,这意味着你的二进制文件会非常庞大,因为你需要为每一种不同的排列组合都在代码库中加入一个kernel。而如果你拥有的是一个更侧重代码生成并具备启发式方法来选择适合kernel的系统,那么你的框架很可能会持续成为人们进行实验探索的平台。这正是PyTorch取得成功的一大关键因素。这种哲学理念在实践中的一个例子基本上就是我们的reduce kernel,所以PyTorch的reduction kernel并不是像我们有一个max.cuh/mean.cuh那样,而是只有一个单独的Reduce.cuh,因为所有的归约操作都具有相同的结构,它们在数学上是高度等价的。我们期望构建一个更为通用的基础设施,其中可以赋予它一个累加器以及一个操作符,然后通过代码生成来获得最优的算法。大家可以详细读一下这个实现 https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/cuda/Reduce.cuh

作者这里做了一些关于 https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/cuda/Reduce.cuh 的笔记,关键点为:

  • 实现是累加器和归约操作无关的(Implementation is accumulator and reduction op agnostic)这意味着实现方式可以适用于不同类型的累加器和归约操作。
  • 使用TensorIterator来迭代张量元素(TensorIterator to iterate over tensor elements)这是一种用于遍历张量数据的机制。
  • ReduceConfig:包含内核启动参数(ReduceConfig: Has kernel launch parameters)如块大小、线程数、网格等,这些参数在setReduceConfig中设置。
  • Reduce_kernel是启动的地方(Reduce_kernel is where it gets launched)
  • 归约策略(Reduction strategies):
    • 线程级(thread level)
    • 块级x,y(block level x,y)
    • 全局归约(global reduce)
  • 矢量化(Vectorization):
    • 可以应用于输入和/或输出

通过学习 https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/cuda/Reduce.cuh 可以掌握很多cuda,模板方面的有趣知识,强烈推荐大家学习一下。

最后,还想展示一下torch.compile生成的reduce kernel。代码为:

# TORCH_LOGS="output_code" python reduce_compile.py
import torch @torch.compile
def f(a):c = torch.sum(a)return cf(torch.randn(10).cuda())

作者展示了一下Triton生成的代码。对代码里面的ReductionHit以及启发式搜索算法在PyTorch中的实现(pytorch/torch/_inductor/triton_heuristics)做了简要浏览,从这里我们也可以发现针对不同的输入大小,调度的kernel类型是不同的。

最后一张Slides提了一下Triton里面是如何实现Reduction的,对应这段代码:

LogicalResultmatchAndRewrite(triton::ReduceOp op, OpAdaptor adaptor,ConversionPatternRewriter &rewriter) const override {ReduceOpHelper helper(op);assert(helper.isSupportedLayout() &&"Unexpected srcLayout in ReduceOpConversion");Location loc = op->getLoc();auto srcValues = unpackInputs(loc, op, adaptor, rewriter);std::map<SmallVector<unsigned>, SmallVector<Value>> accs;std::map<SmallVector<unsigned>, SmallVector<Value>> indices;// First reduce all the values along axis within each thread.reduceWithinThreads(helper, srcValues, accs, indices, rewriter);// Then reduce across threads within a warp.reduceWithinWarps(helper, accs, rewriter);if (helper.isWarpSynchronous()) {// If all the values to be reduced are within the same warp there is// nothing left to do.packResults(helper, accs, rewriter);return success();}// Compute a shared memory base per operand.auto smemShape = helper.getScratchRepShape();SmallVector<Value> smemBases =getSmemBases(op, product<unsigned>(smemShape), rewriter);storeWarpReduceToSharedMemory(helper, accs, indices, smemBases, rewriter);sync(rewriter, loc, op);// The second round of shuffle reduction//   now the problem size: sizeInterWarps, s1, s2, .. , sn//   where sizeInterWarps is 2^m//// Each thread needs to process://   elemsPerThread = sizeInterWarps * s1 * s2 .. Sn / numThreadsaccumulatePartialReductions(helper, smemBases, rewriter);// We could avoid this barrier in some of the layouts, however this is not// the general case.// TODO: optimize the barrier in case the layouts are accepted.sync(rewriter, loc, op);// set output valuesloadReductionAndPackResult(helper, smemShape, smemBases, rewriter);return success();}

算法流程总结为:

  • 初始化和输入处理:
    • 创建一个ReduceOpHelper对象来辅助操作。
    • 解包输入值。
  • 线程内归约:
    • 在每个独立的线程内进行第一轮归约。
    • 这一步可以并行执行,每个线程处理自己的数据部分。
  • Warp内归约:
    • 将线程内归约的结果在warp内进行进一步归约。
    • Warp是GPU中的一个执行单元,通常包含32个线程。
    • 如果所有要归约的值都在同一个warp内,算法在这里就可以结束。
  • 共享内存处理:
    • 如果归约需要跨越多个warp,则使用共享内存来协调更大范围的归约。
    • 计算共享内存的形状和基地址。
    • 将warp内归约的结果存储到共享内存中。
  • 同步:
    • 执行一次同步操作,确保所有线程都完成了前面的步骤。
  • 跨warp归约:
    • 使用共享内存中的数据,执行跨warp的归约操作。
    • 这一步会将不同warp的部分结果进行累积。
  • 再次同步:
    • 再次执行同步操作,确保跨warp归约完成。
  • 最终结果处理:
    • 从共享内存中加载最终的归约结果。
    • 打包结果,准备输出。

总结

这节课是对Redcutions的算法进行了介绍,之前我在【BBuf的CUDA笔记】三,reduce优化入门学习笔记 这里也写过一个Reduce优化的笔记,CUDA-MODE的这个课程更入门和详细一些,Slides的后半部分存在一些适合我们学习的资料,特别是PyTorch的Reducitons.cuh是我们学习Reduce的宝藏。

这篇关于CUDA-MODE课程笔记 第9课: 归约(也对应PMPP的第10章)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1133868

相关文章

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

论文阅读笔记: Segment Anything

文章目录 Segment Anything摘要引言任务模型数据引擎数据集负责任的人工智能 Segment Anything Model图像编码器提示编码器mask解码器解决歧义损失和训练 Segment Anything 论文地址: https://arxiv.org/abs/2304.02643 代码地址:https://github.com/facebookresear

数学建模笔记—— 非线性规划

数学建模笔记—— 非线性规划 非线性规划1. 模型原理1.1 非线性规划的标准型1.2 非线性规划求解的Matlab函数 2. 典型例题3. matlab代码求解3.1 例1 一个简单示例3.2 例2 选址问题1. 第一问 线性规划2. 第二问 非线性规划 非线性规划 非线性规划是一种求解目标函数或约束条件中有一个或几个非线性函数的最优化问题的方法。运筹学的一个重要分支。2

【C++学习笔记 20】C++中的智能指针

智能指针的功能 在上一篇笔记提到了在栈和堆上创建变量的区别,使用new关键字创建变量时,需要搭配delete关键字销毁变量。而智能指针的作用就是调用new分配内存时,不必自己去调用delete,甚至不用调用new。 智能指针实际上就是对原始指针的包装。 unique_ptr 最简单的智能指针,是一种作用域指针,意思是当指针超出该作用域时,会自动调用delete。它名为unique的原因是这个

查看提交历史 —— Git 学习笔记 11

查看提交历史 查看提交历史 不带任何选项的git log-p选项--stat 选项--pretty=oneline选项--pretty=format选项git log常用选项列表参考资料 在提交了若干更新,又或者克隆了某个项目之后,你也许想回顾下提交历史。 完成这个任务最简单而又有效的 工具是 git log 命令。 接下来的例子会用一个用于演示的 simplegit

记录每次更新到仓库 —— Git 学习笔记 10

记录每次更新到仓库 文章目录 文件的状态三个区域检查当前文件状态跟踪新文件取消跟踪(un-tracking)文件重新跟踪(re-tracking)文件暂存已修改文件忽略某些文件查看已暂存和未暂存的修改提交更新跳过暂存区删除文件移动文件参考资料 咱们接着很多天以前的 取得Git仓库 这篇文章继续说。 文件的状态 不管是通过哪种方法,现在我们已经有了一个仓库,并从这个仓

忽略某些文件 —— Git 学习笔记 05

忽略某些文件 忽略某些文件 通过.gitignore文件其他规则源如何选择规则源参考资料 对于某些文件,我们不希望把它们纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。通常它们都是些自动生成的文件,比如日志文件、编译过程中创建的临时文件等。 通过.gitignore文件 假设我们要忽略 lib.a 文件,那我们可以在 lib.a 所在目录下创建一个名为 .gi

取得 Git 仓库 —— Git 学习笔记 04

取得 Git 仓库 —— Git 学习笔记 04 我认为, Git 的学习分为两大块:一是工作区、索引、本地版本库之间的交互;二是本地版本库和远程版本库之间的交互。第一块是基础,第二块是难点。 下面,我们就围绕着第一部分内容来学习,先不考虑远程仓库,只考虑本地仓库。 怎样取得项目的 Git 仓库? 有两种取得 Git 项目仓库的方法。第一种是在本地创建一个新的仓库,第二种是把其他地方的某个

Git 的特点—— Git 学习笔记 02

文章目录 Git 简史Git 的特点直接记录快照,而非差异比较近乎所有操作都是本地执行保证完整性一般只添加数据 参考资料 Git 简史 众所周知,Linux 内核开源项目有着为数众多的参与者。这么多人在世界各地为 Linux 编写代码,那Linux 的代码是如何管理的呢?事实是在 2002 年以前,世界各地的开发者把源代码通过 diff 的方式发给 Linus,然后由 Linus