Numba 的 CUDA 示例(1/4):踏上并行之旅

news/2024/7/20 18:42:13/文章来源:https://blog.csdn.net/ivandoo/article/details/139281590

按照本系列从头开始使用 Python 学习 CUDA 编程

介绍

GPU(图形处理单元),顾名思义,最初是为计算机图形学开发的。从那时起,它们几乎在每个需要高计算吞吐量的领域都无处不在。这一进步得益于 GPGPU(通用 GPU)接口的发展,这些接口使我们能够对 GPU 进行编程以进行通用计算。这些接口中最常见的是CUDA,其次是OpenCL,最近的是 HIP。

img

Python 中的 CUDA

CUDA 最初设计为与 C 兼容。后来的版本将其扩展到 C++ 和 Fortran。在 Python 生态系统中,使用 CUDA 的方法之一是通过Numba,这是一个适用于 Python 的即时 (JIT) 编译器,可以针对 GPU(它也针对 CPU,但这超出了我们的范围)。使用 Numba,可以直接用 Python(一个子集)编写内核,Numba 将即时编译代码并运行它。虽然它没有实现完整的 CUDA API,但其支持的功能通常足以获得与 CPU 相比令人印象深刻的加速(有关所有缺失的功能,请参阅Numba 文档1)。

然而, Numba并不是唯一的选择。CuPy既提供依赖于 CUDA 的高级函数,也提供用于集成用 C 编写的内核的低级 CUDA 支持,以及可 JIT 的 Python 函数(类似于 Numba)。PyCUDA 提供了对 CUDA API 的更细粒度控制。最近,Nvidia 发布了官方CUDA Python,这必将丰富生态系统。所有这些项目都可以相互传递设备数组,你不必局限于只使用一个。

在本系列中

本系列的目标是通过用 Numba CUDA 编写的示例为常见的 CUDA 模式提供一个学习平台。本系列并不是 CUDA 或 Numba 的综合指南。读者可以参考它们各自的文档。本教程的结构受到Jason Sanders 和 Edward Kandrot 合著的[CUDA by Example: An Introduction to General-Purpose GPU Programming](https://developer.nvidia.com/cuda-example)一书的启发。如果你最终不再使用 Python 并想用 C 语言编写代码,那么这是一个极好的资源。

本系列还将会包含第 2 部分、第 3 部分和第 4 部分。

在本教程中,我们将学习如何运行我们的第一个 Numba CUDA 内核。我们还将学习如何有效地使用 CUDA 执行高度并行的任务,即完全相互独立的任务。最后,我们将学习如何从 CPU 计时内核的运行时间。因为我的电脑是 Mac,无法实现 CUDA,所以我在 Google Colab 上进行了实现,可以点击查看:https://colab.research.google.com/drive/1O5bhDHZgJqLwVaQl4rkoewY_XyO_krKQ?usp=sharing

GPU 并行编程简介

GPU 相对于 CPU 的最大优势在于它们能够并行执行相同的指令。单个 CPU 内核将以串行方式一个接一个地运行指令。在 CPU 上进行并行化需要同时使用其多个内核(物理或虚拟)。标准的现代计算机具有 4-8 个内核。另一方面,现代 GPU 拥有数百甚至数千个计算内核。参见图 1 以了解两者之间的比较。GPU 内核通常较慢并且只能执行简单指令,但它们的庞大数量通常可以弥补这些缺点。需要注意的是,为了使 GPU 比 CPU 更具优势,它们运行的算法必须是可并行的。

我认为理解 GPU 编程有四个主要方面。第一个我已经提到过:理解如何思考和设计本质上并行的算法。这可能很难,因为有些算法是串行设计的,也因为可以有多种并行化同一算法的方法。

第二个方面是学习如何将主机上的结构(例如矢量和图像)映射到 GPU 结构(例如线程和块)上。重复模式和辅助函数可以帮助我们实现这一点,但归根结底,实验对于充分利用 GPU 至关重要。

第三是理解驱动 GPU 编程的异步执行模型。不仅 GPU 和 CPU 彼此独立地执行指令,GPU 还具有允许多个处理流在同一 GPU 中运行的流。这种异步性在设计最佳处理流程时非常重要。

第四个也是最后一个方面是抽象概念和具体代码之间的关系:这是通过学习 API 及其细微差别来实现的。

当你阅读第一章时,请尝试在以下示例中识别这些概念!

图 1

图 1.1。简化的 CPU 架构(左)和 GPU 架构(右)。算术发生在 ALU(算术逻辑单元)中,DRAM 数据,缓存保存可以更快访问的数据,但通常容量较小。控制单元执行指令。来源:维基百科。

入门

我们将首先设置我们的环境:高于 0.55 的 Numba 版本和受支持的 GPU。

import numpy as np
import numba
from numba import cudaprint(np.__version__)
print(numba.__version__)---
1.25.2
0.59.1cuda.detect()---
Found 1 CUDA devices
id 0             b'Tesla T4'                              [SUPPORTED]Compute Capability: 7.5PCI Device ID: 4PCI Bus ID: 0UUID: GPU-0aa3c43c-1ada-6075-e57a-dccb0793a8b6Watchdog: DisabledFP32/FP64 Performance Ratio: 32
Summary:1/1 devices are supported
True

Numba CUDA 的主要工作是cuda.jit装饰器。它用于定义将在 GPU 中运行的函数。

我们首先定义一个简单的函数,该函数接受两个数字并将它们存储在第三个参数的第一个元素上。我们的第一课是内核(启动线程的 GPU 函数)不能返回值。我们通过传递输入输出来解决这个问题。这是 C 中的常见模式,但在 Python 中并不常见。

# Example 1.1: Add scalars
@cuda.jit
def add_scalars(a, b, c):c[0] = a + bdev_c = cuda.device_array((1,), np.float32)add_scalars[1, 1](2.0, 7.0, dev_c)c = dev_c.copy_to_host()
print(f"2.0 + 7.0 = {c[0]}")---
2.0 + 7.0 = 9.0

你可能已经注意到,在调用内核之前,我们需要在设备上分配一个数组。此外,如果我们想显示返回的值,我们需要将其复制回 CPU。你可能会问自己为什么我们选择分配一个float32(单精度浮点数)。这是因为,虽然大多数现代 GPU 都支持双精度运算,但双精度运算所需的时间可能是单精度运算的 4 倍或更长。因此,最好习惯使用np.float32andnp.complex64而不是float/np.float64complex/ np.complex128

虽然内核定义看起来类似于 CPU 函数,但内核调用略有不同。特别是,它在参数前有方括号:

add_scalars[1, 1](2.0, 7.0, dev_c)

这些方括号分别表示网格中的块数和块中的线程数。在学习使用 CUDA 进行并行化时,让我们进一步讨论一下这些含义。

使用 CUDA 进行并行化

CUDA 网格的剖析

启动内核时,它会有一个与之关联的网格。网格由块组成;块由线程组成。图 2 显示了一维 CUDA 网格。图中的网格有 4 个块。网格中的块数保存在一个特殊变量中,该变量可在内核内部访问,称为gridDim.x.x是指网格的第一维(在本例中是唯一的一维)。二维网格也有.y和三维网格.z变量。同样在内核内部,你可以通过使用 找出正在执行哪个块blockIdx.x,在本例中它将从 0 运行到 3。

每个块都有一定数量的线程,保存在变量中blockDim.x。线程索引保存在变量中threadIdx.x,在本例中从 0 到 7。

重要的是,不同块中的线程被安排以不同的方式运行,可以访问不同的内存区域,并且在某些方面也有所不同(请参阅CUDA 复习:CUDA 编程模型的简要讨论)。现在,我们将跳过这些细节。

图 2:一维 CUDA 网格

当我们在第一个示例中使用参数启动内核时[1, 1],我们告诉 CUDA 运行一个块和一个线程。传递多个块和多个线程将多次运行内核。操纵threadIdx.xblockIdx.x将使我们能够唯一地标识每个线程。

我们不再对两个数字求和,而是尝试对两个数组求和。假设每个数组有 20 个元素。如上图所示,我们可以启动一个内核,每个块有 8 个线程。如果我们希望每个线程只处理一个数组元素,那么我们将至少需要 4 个块。启动 4 个块,每个块有 8 个线程,我们的网格将启动 32 个线程。

现在我们需要弄清楚如何将线程索引映射到数组索引。threadIdx.x从 0 到 7 运行,因此它们本身无法索引我们的数组。此外,不同的块具有相同的threadIdx.x。另一方面,它们具有不同的blockIdx.x。要为每个线程获取唯一索引,我们可以组合这些变量:

i = threadIdx.x + blockDim.x * blockIdx.x

对于第一个块,blockIdx.x = 0i将从 0 运行到 7。对于第二个块,blockIdx.x = 1。由于blockDim.x = 8i将从 8 运行到 15。同样,对于blockIdx.x = 2i将从 16 运行到 23。在第四个也是最后一个块中,i将从 24 运行到 31。请参阅下表 1。

i01234567891011121314151631
threadIdx.x012345670123456707
blockIdx.x000000001111111123

我们解决了一个问题:如何将每个线程映射到数组中的每个元素……但现在我们遇到了一个问题,即某些线程会溢出数组,因为数组有 20 个元素,最多i可达 32-1。解决方案很简单:对于这些线程,不要执行任何操作!

我们来看看代码。

# Example 1.2: Add arrays
@cuda.jit
def add_array(a, b, c):i = cuda.threadIdx.x + cuda.blockDim.x * cuda.blockIdx.xif i < a.size:c[i] = a[i] + b[i]N = 20
a = np.arange(N, dtype=np.float32)
b = np.arange(N, dtype=np.float32)
dev_c = cuda.device_array_like(a)add_array[4, 8](a, b, dev_c)c = dev_c.copy_to_host()
print(c)---
[ 0.  2.  4.  6.  8. 10. 12. 14. 16. 18. 20. 22. 24. 26. 28. 30. 32. 34. 36. 38.]

在较新版本的 Numba 中,我们会收到一条警告,指出我们使用主机数组调用了内核。理想情况下,我们希望避免将数据从主机移动到设备,因为这非常慢。我们应该在所有参数中使用设备数组调用内核。我们可以通过预先将数组从主机移动到设备来实现这一点:

dev_a = cuda.to_device(a)
dev_b = cuda.to_device(b)

此外,每个线程的唯一索引的计算很快就会过时。值得庆幸的是,Numba 提供了非常简单的包装器cuda.grid,它以网格维度作为唯一参数来调用。新内核将如下所示:

# Example 1.3: Add arrays with cuda.grid
@cuda.jit
def add_array(a, b, c):i = cuda.grid(1)if i < a.size:c[i] = a[i] + b[i]add_array[4, 8](dev_a, dev_b, dev_c)c = dev_c.copy_to_host()
print(c)---
[ 0.  2.  4.  6.  8. 10. 12. 14. 16. 18. 20. 22. 24. 26. 28. 30. 32. 34. 36. 38.]

当我们改变数组的大小时会发生什么?一种简单的解决方法是简单地更改网格参数(块数和每个块的线程数),以便至少启动与数组中的元素一样多的线程。

设置这些参数既需要一定的科学性,也需要一定的艺术性。从“科学性”的角度来说,我们会说 (a) 它们应该是 2 的倍数,通常在 32 到 1024 之间,以及 (b) 应该选择它们以最大化占用率(同时有多少个线程处于活动状态)。Nvidia 提供了一个电子表格来帮助计算这些值。从“艺术性”的角度来说,没有什么可以预测内核的行为,因此如果你真的想优化这些参数,你需要使用典型输入来分析你的代码。实际上,现代 GPU 的“合理”线程数是 256。

N = 1_000_000
a = np.arange(N, dtype=np.float32)
b = np.arange(N, dtype=np.float32)dev_a = cuda.to_device(a)
dev_b = cuda.to_device(b)
dev_c = cuda.device_array_like(a)threads_per_block = 256
blocks_per_grid = (N + (threads_per_block - 1)) // threads_per_block
# Note that
#     blocks_per_grid == ceil(N / threads_per_block)
# ensures that blocks_per_grid * threads_per_block >= Nadd_array[blocks_per_grid, threads_per_block](dev_a, dev_b, dev_c)c = dev_c.copy_to_host()
np.allclose(a + b, c)---
True

在继续讨论向量求和之前,我们需要讨论一下硬件限制。GPU 无法运行任意数量的线程和块。通常,每个块不能有超过 1024 个线程,并且网格不能有超过 2¹⁶ − 1 = 65535 个块。这并不是说你可以启动 1024 × 65535 个线程……根据其寄存器占用的内存量以及其他考虑因素,可以启动的线程数量是有限制的。此外,必须谨慎尝试处理无法一次性放入 GPU RAM 的大型数组。在这些情况下,可以使用单个 GPU 或多个 GPU 分段处理数组。

信息:在 Python 中,可以通过 Nvidia 的 cuda-python 库,通过其文档中的函数 cuDeviceGetAttribute 获取硬件限制。有关示例,请参见本节末尾的附录。

网格跨步循环

如果每个网格的块数超出硬件限制,但数组适合内存,则我们可以使用一个线程来处理多个元素,而不是每个数组元素使用一个线程。我们将使用一种称为网格步长循环的技术来实现这一点。除了克服硬件限制之外,网格步长循环内核还可以通过重用线程来最大限度地减少线程创建/销毁开销。Mark Harris 的博客文章 CUDA Pro Tip: Write Flexible Kernels with Grid-Stride Loops 详细介绍了网格步长循环的一些好处。

该技术背后的想法是在 CUDA 内核中添加一个循环来处理多个输入元素。顾名思义,此循环的步幅等于网格中的线程数。这样,如果网格中的线程总数 ( threads_per_grid = blockDim.x * gridDim.x) 小于数组元素的数量,则内核处理完索引后cuda.grid(1)就会处理索引cuda.grid(1) + threads_per_grid,依此类推,直到处理完所有数组元素。事不宜迟,让我们看看代码。

# Example 1.4: Add arrays with grid striding
@cuda.jit
def add_array_gs(a, b, c):i_start = cuda.grid(1)threads_per_grid = cuda.blockDim.x * cuda.gridDim.xfor i in range(i_start, a.size, threads_per_grid):c[i] = a[i] + b[i]threads_per_block = 256
blocks_per_grid_gs = 32 * 80  # Use 32 * multiple of streaming multiprocessors
# 32 * 80 * 256 < 1_000_000 so one thread will process more than one array elementadd_array_gs[blocks_per_grid_gs, threads_per_block](dev_a, dev_b, dev_c)
c = dev_c.copy_to_host()
np.allclose(a + b, c)---
True

这段代码与上面的代码非常相似,不同之处在于我们从 cuda.grid(1) 开始,但会执行更多的采样,每个threads_per_grid 执行一次,直到数组结束。

那么,哪一个内核更快呢?

计时 CUDA 内核

GPU 编程的核心在于速度。因此,准确测量代码执行情况非常重要。

CUDA 内核是主机 (CPU)启动的设备功能,但它们当然是在 GPU 上执行的。除非我们告诉它们,否则 GPU 和 CPU 不会进行通信。因此,当 GPU 内核启动时,CPU 将继续运行指令,无论是启动更多内核还是执行其他 CPU 功能。如果我们time.time()在内核启动之前和之后进行调用,我们将仅计时内核启动所需的时间*,而不是运行所需的*时间。

我们可以使用一个函数来确保 GPU 已经“赶上” 。cuda.synchronize(),调用此函数将停止主机执行任何其他代码,直到 GPU 完成已在其中启动的每个内核的执行。

要对内核执行进行计时,我们可以简单地计时内核运行然后同步所需的时间。这有两个注意事项。首先,我们需要使用time.perf_counter()time.perf_counter_ns()而不是time.time()time.time()不计算主机休眠等待 GPU 完成执行的时间。第二个注意事项是,从主机计时代码并不理想,因为这会产生相关开销。稍后,我们将解释如何使用 CUDA事件对来自设备的内核进行计时。Mark Harris 有另一篇关于此主题的精彩博客文章,标题为How to Implement Performance Metrics in CUDA C/C++

使用 Numba 时,我们必须注意一个细节。Numba 是一个即时编译器,这意味着函数只有在被调用时才会被编译。因此,对函数的第一次调用进行计时*也会对编译步骤进行计时,*而编译步骤通常要慢得多。我们必须记住始终先通过启动内核然后同步它来编译代码,以确保 GPU 中没有剩余的内容要运行。这可确保下一个内核无需编译即可立即运行。还要注意,数组的dtype应该相同,因为 Numba 为参数的每种组合编译一个唯一的函数dtypes

from time import perf_counter_ns# Compile and then clear GPU from tasks
add_array[blocks_per_grid, threads_per_block](dev_a, dev_b, dev_c)
cuda.synchronize()timing = np.empty(101)
for i in range(timing.size):tic = perf_counter_ns()add_array[blocks_per_grid, threads_per_block](dev_a, dev_b, dev_c)cuda.synchronize()toc = perf_counter_ns()timing[i] = toc - tic
timing *= 1e-3  # convert to μsprint(f"Elapsed time: {timing.mean():.0f} ± {timing.std():.0f} μs")---
Elapsed time: 131 ± 36 μs
# Compile and then clear GPU from tasks
add_array_gs[blocks_per_grid_gs, threads_per_block](dev_a, dev_b, dev_c)
cuda.synchronize()timing_gs = np.empty(101)
for i in range(timing_gs.size):tic = perf_counter_ns()add_array_gs[blocks_per_grid_gs, threads_per_block](dev_a, dev_b, dev_c)cuda.synchronize()toc = perf_counter_ns()timing_gs[i] = toc - tic
timing_gs *= 1e-3  # convert to μsprint(f"Elapsed time: {timing_gs.mean():.0f} ± {timing_gs.std():.0f} μs")---
Elapsed time: 140 ± 36 μs

对于简单内核,我们还可以测量算法的吞吐量,它等于每秒浮点运算次数。它通常以 GFLOP/s(每秒千兆 FLOP)为单位。我们的加法运算仅包含一个 FLOP:加法。因此,吞吐量由以下公式给出:

#              G * FLOP       / timing in s
gflops    = 1e-9 * dev_a.size * 1e6 / timing.mean()
gflops_gs = 1e-9 * dev_a.size * 1e6 / timing_gs.mean()print(f"GFLOP/s (algo 1): {gflops:.2f}")
print(f"GFLOP/s (algo 2): {gflops_gs:.2f}")---
GFLOP/s (algo 1): 7.65
GFLOP/s (algo 2): 7.15

2D 示例

为了结束本教程,让我们制作一个 2D 内核来对图像应用对数校正。

给定一个图像 I(x, y),其值介于 0 和 1 之间,对数校正图像由下式给出

Iᵪ(x, y) = γ log₂ (1 + I(x, y))

首先让我们获取一些数据!

import matplotlib.pyplot as plt
from skimage import datamoon = data.moon().astype(np.float32) / 255.fig, ax = plt.subplots()
im = ax.imshow(moon, cmap="gist_earth")
ax.set_xticks([])
ax.set_yticks([])
ax.set_xticklabels([])
ax.set_yticklabels([])
fig.colorbar(im)
fig.tight_layout()

图 3。原始“月球”数据集

如你所见,数据在低端确实饱和了,几乎没有高于 0.6 的值。

让我们编写内核。

import math# Example 1.5: 2D kernel
@cuda.jit
def adjust_log(inp, gain, out):ix, iy = cuda.grid(2) # The first index is the fastest dimensionthreads_per_grid_x, threads_per_grid_y = cuda.gridsize(2) #  threads per grid dimensionn0, n1 = inp.shape # The last index is the fastest dimension# Stride each dimension independentlyfor i0 in range(iy, n0, threads_per_grid_y):for i1 in range(ix, n1, threads_per_grid_x):out[i0, i1] = gain * math.log2(1 + inp[i0, i1])threads_per_block_2d = (16, 16)  #  256 threads total
blocks_per_grid_2d = (64, 64)moon_gpu = cuda.to_device(moon)
moon_corr_gpu = cuda.device_array_like(moon_gpu)adjust_log[blocks_per_grid_2d, threads_per_block_2d](moon_gpu, 1.0, moon_corr_gpu)
moon_corr = moon_corr_gpu.copy_to_host()fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.imshow(moon, cmap="gist_earth")
ax2.imshow(moon_corr, cmap="gist_earth")
ax1.set(title="Original image")
ax2.set(title="Log-corrected image")
for ax in (ax1, ax2):ax.set_xticks([])ax.set_yticks([])ax.set_xticklabels([])ax.set_yticklabels([])
fig.tight_layout()

让我们注意一下这两个 for 循环。请注意,第一个 for 循环从 iy 开始,而最内层的第二个循环从 ix 开始。我们完全可以选择 i0ix 处开始,而 i1iy 处开始,这样会感觉更自然。那么,我们为什么要选择这种顺序呢?事实证明,第一种选择的内存访问模式更有效。由于第一个网格索引是最快的索引,我们希望它能与我们最快的维度(即最后一个维度)相匹配。

如果你不想相信我的话(你不应该相信!)你现在已经学会了如何对内核执行进行计时,你可以尝试这两个版本。对于像这里使用的数组这样的小数组,差异可以忽略不计,但对于较大的数组(例如 10,000 x 10,000),我测量到的速度提高了约 10%。虽然不是特别令人印象深刻,但如果我可以通过一次变量交换为你提供 10% 的改进,谁会不接受呢?

就这样!我们现在可以在校正后的图像中看到更多细节。

作为练习,尝试使用不同的网格对不同的启动进行计时,以找到适合你的机器的最佳网格大小。

图 4。原始(左)和对数校正(右)“月球”数据集

结论

在本教程中,你学习了 Numba CUDA 的基础知识。你学习了如何创建简单的 CUDA 内核,并将内存移至 GPU 以使用它们。你还学习了如何使用一种称为grid-stride loops 的技术迭代 1D 和 2D 数组。

附录:使用 Nvidia 的 cuda-python 探测设备属性

为了对 GPU 的精确属性进行细粒度控制,你可以依赖 Nvidia 提供的低级官方 CUDA Python 包。

# Need to: pip install --upgrade cuda-pythonfrom cuda.cuda import CUdevice_attribute, cuDeviceGetAttribute, cuDeviceGetName, cuInit# Initialize CUDA Driver API
(err,) = cuInit(0)# Get attributes
err, DEVICE_NAME = cuDeviceGetName(128, 0)
DEVICE_NAME = DEVICE_NAME.decode("ascii").replace("\x00", "")err, MAX_THREADS_PER_BLOCK = cuDeviceGetAttribute(CUdevice_attribute.CU_DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK, 0
)
err, MAX_BLOCK_DIM_X = cuDeviceGetAttribute(CUdevice_attribute.CU_DEVICE_ATTRIBUTE_MAX_BLOCK_DIM_X, 0
)
err, MAX_GRID_DIM_X = cuDeviceGetAttribute(CUdevice_attribute.CU_DEVICE_ATTRIBUTE_MAX_GRID_DIM_X, 0
)
err, SMs = cuDeviceGetAttribute(CUdevice_attribute.CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT, 0
)print(f"Device Name: {DEVICE_NAME}")
print(f"Maximum number of multiprocessors: {SMs}")
print(f"Maximum number of threads per block: {MAX_THREADS_PER_BLOCK:10}")
print(f"Maximum number of blocks per grid:   {MAX_BLOCK_DIM_X:10}")
print(f"Maximum number of threads per grid:  {MAX_GRID_DIM_X:10}")---
Device Name: Tesla T4                                                                
Maximum number of multiprocessors: 40
Maximum number of threads per block:       1024
Maximum number of blocks per grid:         1024
Maximum number of threads per grid:  2147483647

  1. https://numba.readthedocs.io/en/stable/cuda/overview.html#missing-cuda-features ↩︎

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

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

相关文章

关于微信小程序低功耗蓝牙ECharts实时刷新(涉及自定义缓冲区)

简单的蓝牙显示&#xff08;串口手动发数据测试&#xff09; 最近搞了这方面的东西&#xff0c;是刚刚开始接触微信小程序&#xff0c;因为是刚刚开始接触蓝牙设备&#xff0c;所以这篇文章适合既不熟悉小程序&#xff0c;又不熟悉蓝牙的新手看。 项目要求是获取到蓝牙传输过来…

JEPaaS 低代码平台 accessToTeanantInfo SQL注入漏洞复现

0x01 产品简介 JEPaaS低代码开发平台开源版 旨在帮助企业快速实现信息化和数字化转型。该平台基于可视化开发环境,让软件开发人员和业务用户通过直观的可视化界面来构建应用程序 ,而不是传统的编写代码方式。 用户可以在开发平台灵活各个图形化控件,以构建业务流程、逻辑和…

pytorch深度学习-环境搭建-2

1.1下载cudnn,解压 1.2.找到本级cuda安装路径 1.3.刚才解压文件复制到cuda安装目录 2.1 安装pytouch

Day23:Leetcode:530.二叉搜索树的最小绝对差 + 501.二叉搜索树中的众数 + 236. 二叉树的最近公共祖先

LeetCode&#xff1a;530.二叉搜索树的最小绝对差 问题描述 解决方案&#xff1a; 1.思路 中序遍历 2.代码实现 class Solution {int pre;int ans;public int getMinimumDifference(TreeNode root) {ans Integer.MAX_VALUE;pre -1;dfs(root);return ans;}public void d…

全球首例光伏电场网络攻击事件曝光

快速增长的光伏发电正面临日益严重的网络安全威胁。近日&#xff0c;日媒报道了首个针对光伏电场的网络攻击事件。 首例公开确认的光伏电网攻击 日本媒体《产经新闻》近日报道&#xff0c;黑客劫持了一个大型光伏电网中的800台远程监控设备(由工控电子制造商Contec生产的Solar…

2024HBCPC:E Breakfast II

题目描述 作为一个合格的大学生&#xff0c;你不仅需要学习成绩好&#xff0c;还需要会买包子和鸡蛋。 今天&#xff0c;又轮到你们给你的导师买早饭了&#xff01; 这一次你们一共需要给导师买 n n n 个包子和 m m m 个鸡蛋&#xff08;请注意&#xff0c;这一次可能不再只…

网关(GateWay)- 快速使用

引入依赖 <!-- gateway --> <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId> </dependency> 路由配置 server:port: 8088 spring:application:name: api-gatew…

Clickhouse Bitmap 类型操作总结—— Clickhouse 基础篇(四)

文章目录 创建 Bitmap 对象Bitmap 转换为整数数组计算总数&#xff08;去重&#xff09;值指定start, end 索引生成子 Bitmap指定 start 索引和数量限制生成子 Bitmap指定偏移量生成子 Bitmap是否包含指定元素两个 Bitmap 是否存在相同元素一个是否为另一个 Bitmap 的子集求最小…

25 使用MapReduce编程了解垃圾分类情况

测试数据中1表示可回收垃圾&#xff0c;2表示有害垃圾&#xff0c;4表示湿垃圾&#xff0c;8表示干垃圾。 统计数据中各类型垃圾的数量&#xff0c;分别存储可回收垃圾、有害垃圾、湿垃圾和干垃圾的统计结果。 &#xff08;存储到4个不同文件中&#xff0c;垃圾信息&#xff0…

VsCode创建Python虚拟环境

不同的项目&#xff0c;大多使用不同的版本与包&#xff0c;为每个项目创建不同的环境&#xff0c;可以防止版本等的不同而带来的影响。 Python自3.3版本之后&#xff0c;官方自带了用于创建虚拟环境的venv模块&#xff0c;以下将介绍venv模块在Python虚拟环境的用法。 1.安装相…

合约构成-成员变量、函数、事件event、修饰器modifier及构造函数

合约的基本结构 合约中的成员变量合约中的成员函数Event&#xff08;事件&#xff09;、modifier(修饰器)与constructor&#xff08;构造函数:实例产生的时候执行&#xff09; Event事件 modifier construcor 1、成员变量 概念&#xff1a;存储合约状态的变量 声明方法&a…

100个 Unity小游戏系列六 -Unity 抽奖游戏专题四 翻卡游戏

一、演示效果 二、知识点讲解 2.1 布局 void CreateItems(){reward_data_list reward_data_list ?? new List<RewardData>();reward_data_list.Clear();for (int i 0; i < ItemCount; i){GameObject item;if (i 1 < itemParent.childCount){item itemParent…

母婴商城购物网站,基于 SpringBoot+Vue+MySQL 开发的前后端分离的母婴商城购物网站设计实现

目录 一. 前言 二. 功能模块 2.1. 前台功能 2.2. 用户信息管理 2.3. 商品分类管理 2.4. 商品信息管理 2.5. 商品资讯管理 三. 部分代码实现 四. 源码下载 一. 前言 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&a…

多输入多输出 | MATLAB实现BiTCN(双向时间卷积神经网络)多输入多输出预测

多输入多输出 | MATLAB实现BiTCN(双向时间卷积神经网络)多输入多输出预测 目录 多输入多输出 | MATLAB实现BiTCN(双向时间卷积神经网络)多输入多输出预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab实现BiTCN双向时间卷积神经网络多输入多输出预测 1.data为数据…

Mac连接虚拟机(Linux系统)

1.确定虚拟机的IP地址 ifconfig //终端命令&#xff0c;查询ip地址 sudo apt install net-tools 安装完成后再次执行 ifconfig&#xff1a; 2.安装SSH&#xff08;加密远程登录协议&#xff09; (1).安装OpenSSH服务器软件包&#xff1a; sudo apt-get install openssh-ser…

【最优化方法】实验一 熟悉MATLAB基本功能

实验一  熟悉MATLAB基本功能 实验的目的和要求&#xff1a;在本次实验中&#xff0c;通过亲临使用MATLAB&#xff0c;对该软件做一全面了解并掌握重点内容。 实验内容&#xff1a; &#xff11;、全面了解MATLAB系统 &#xff12;、实验常用工具的具体操作和功能 学习建…

机器学习大模型驱动:未来的趋势与应用

文章目录 &#x1f4d1;前言一、什么是机器学习大模型&#xff1f;1.1 大模型的特点1.2 大模型的技术基础 二、大模型的技术实现2.1 Transformer 架构2.2 预训练和微调2.3 模型并行和数据并行 三、大模型的应用场景3.1 自然语言处理&#xff08;NLP&#xff09;3.2 计算机视觉&…

vue学习汇总

目录 一、vue基本语法 1.插值表达式 {{}} 2.显示数据(v-text)和(v-html) 3.事件处理(v-on) 4.循环遍历(v-for) 5.判断语法(v-if) 6.元素显示与隐藏(v-show) 7.动态设置属性(v-bind) 8.数据双向绑定(v-model) 9.计算属性 二、vue组件 1.使用组件的三个步骤 2.注册组…

采用Java+ SpringBoot+ IntelliJ+idea开发的ADR药物不良反应监测系统源码

采用Java SpringBoot IntelliJidea开发的ADR药物不良反应监测系统源码 ADR药物不良反应监测系统有哪些应用场景&#xff1f; ADR药物不良反应监测系统有哪些应用场景&#xff1f; ADR药物不良反应监测系统具有广泛的应用场景&#xff0c;以下是一些主要的应用场景&#xff1a…

建立FTP服务器

文章目录 建立FTP服务器1. 使用VMware安装CentOS 7虚拟机。2. 安装完虚拟机后&#xff0c;进入虚拟机&#xff0c;修改网络配置&#xff08;onboot改为yes&#xff09;并重启网络服务&#xff0c;查看相应IP地址&#xff0c;并使用远程连接软件进行连接。3.配置yum源&#xff0…