随着深度学习模型复杂度和数据集规模的增大,计算效率成为了不可忽视的问题。GPU凭借强大的并行计算能力,成为深度学习加速的标配。然而,由于服务器的显存非常有限,随着训练样本越来越大,显存连一个样本都容不下的现象频频发生。除了升级硬件(烧钱)、使用分布式训练(费力),你知道还有哪些方法吗?即使显存充足,所有运算都在GPU上执行就是最高效吗?只要掌握以下小知识,模型训练的种种问题统统搞定,省时省力省钱,重点是高效

其实CPU和GPU是协同工作的,如果能合理地利用它们各自的优势,就能够节省显存资源(显存不够内存来凑),甚至获得更好的训练性能。本文为您提供了device_guard接口,只需要一行命令,即可实现GPU和CPU的混合训练,不仅可以解决训练模型时通过调整批尺寸(batch size)显存依然超出的问题,让原本无法在单台服务器执行的模型可以训练,同时本文还给出了提高GPU和CPU混合训练效率的方法,将服务器资源利用到极致,帮助您提升模型的性能!

模型训练的特点

深度学习任务通常使用GPU进行模型训练。这是因为GPU相对于CPU具有更多的算术逻辑单元(ALU),可以发挥并行计算的优势,特别适合计算密集型任务,可以更高效地完成深度学习模型的训练。GPU模式下的模型训练如图1所示,总体可以分为4步:

第1步,将输入数据从系统内存拷贝到显存。

第2步,CPU指示GPU处理数据。

第3步,GPU并行地完成一系列的计算。

第4步,将计算结果从显存拷贝到内存。

图1 模型训练示意图

从图中可以了解到,虽然GPU并行计算能力优异,但无法单独工作,必须由CPU进行控制调用;而且显存和内存之间的频繁数据拷贝也可能会带来较大的性能开销。CPU虽然计算能力不如GPU,但可以独立工作,可以直接访问内存数据完成计算。因此,想获得更好的训练性能,需要合理利用GPU和CPU的优势。

模型训练的常见问题

问题一:GPU显存爆满,资源不足

你建的模型不错,在这个简洁的任务中可能成为新的SOTA,但每次尝试批量处理更多样本时,你都会得到一个CUDA RuntimeError:out of memory。

这是因为GPU卡的显存是非常有限的,一般远低于系统内存。以V100为例,其显存最高也仅有32G,甚至有些显存仅12G左右。因此当模型的参数量较大时,在GPU模式下模型可能无法训练起来。

设置CPU模式进行模型训练,可以避免显存不足的问题,但是训练速度往往太慢。

那么有没有一种方法,可以在单机训练中充分地利用GPU和CPU资源,让部分层在CPU执行,部分层在GPU执行呢?

问题二:频繁数据拷贝,训练效率低

在显存足够的情况下,我们可以直接采用GPU模式去训练模型,但是让所有的网络层都运行在GPU上就一定最高效吗?其实GPU只对特定任务更快,而CPU擅长各种复杂的逻辑运算。框架中有一些OP会默认在CPU上执行,或者有一些OP的输出会被存储在CPU上,因为这些输出往往需要在CPU上访问。这就会导致训练过程中,CPU和GPU之间存在数据拷贝。

图2是CPU和GPU数据传输示意图。假设模型的中间层存在下图中的4个算子。其中算子A和算子B都在CPU执行,因此B可以直接使用A的输出。算子C和算子D都在GPU上执行,那么算子D也可以直接使用C的输出。但是算子B执行完,其输出在CPU上,在算子C执行时,就会将B的输出从CPU拷贝到GPU。

频繁的数据拷贝,也会影响模型的整体性能。如果能把算子A和B设置在GPU上执行,或者算子C和D设置在CPU上执行,避免数据传输,或许会提升模型性能。那么应该如何更加合理地为算子分配设备,使得训练过程更加高效呢?我们需要更综合地考虑,在发挥GPU和CPU各自计算优势的前提下,降低数据拷贝带来的时间消耗。

图2 CPU和GPU数据传输示意图

device_guard自定义GPU和CPU混合训练

上面两个场景都是希望为模型中的部分层指定运行设备。飞桨提供了fluid.CUDAPlace和fluid.CPUPlace用于指定运行设备,但这两个接口在指定设备时是二选一的,也就是说要么在GPU模式下训练,要么在CPU模式下训练。过去我们无法指定某一部分计算在GPU上执行还是在CPU上执行。飞桨开源框架从1.8版本开始提供了device_guard接口,使用该接口可以为网络中的计算层指定设备为CPU或者GPU,实现更灵活的异构计算调度。

如何使用device_guard接口解决上面两个场景中提到的问题呢?接下来,我们看看具体的例子。

好处一:充分利用CPU资源,避免显存超出

如果使用fluid.CUDAPlace指定了全局的运行设备,飞桨将会自动把支持GPU计算的OP分配在GPU上执行,然而当模型参数量过大并且显存有限时,很可能会遇到显存超出的情况。如下面的示例代码,embedding层的参数size包含两个元素,第一个元素为vocab_size(词表大小),第二个为emb_size(embedding层维度)。实际场景中,词表可能会非常大。示例代码中,词表大小被设置为10,000,000,该层创建的权重矩阵的大小为(10000000, 150),仅这一层就需要占用5.59G的显存。如果再加上其他的网络层,在这种大词表场景下,很有可能会显存超出。

import paddle.fluid as fluiddata = fluid.layers.fill_constant(shape=[1], value=128, dtype='int64')
label = fluid.layers.fill_constant(shape=[1, 150], value=0.5, dtype='float32')
emb = fluid.embedding(input=data, size=(10000000, 150), dtype='float32')
out = fluid.layers.l2_normalize(x=emb, axis=-1)cost = fluid.layers.square_error_cost(input=out, label=label)
avg_cost = fluid.layers.mean(cost)
sgd_optimizer = fluid.optimizer.SGD(learning_rate=0.001)
sgd_optimizer.minimize(avg_cost)place = fluid.CUDAPlace(0)
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())
result = exe.run(fluid.default_main_program(), fetch_list=[avg_cost])

embedding是根据input中的id信息从embedding矩阵中查询对应embedding信息,它并不是一个计算密度非常高的OP,因此在CPU上进行计算,其速度也是可接受的。如果将embedding层设置在CPU上运行,就能够充分利用CPU大内存的优势,避免显存超出。可以参考如下代码,使用device_guard将embedding层设置在CPU上。那么,除了embedding层,其他各层都会在GPU上运行。

import paddle.fluid as fluiddata = fluid.layers.fill_constant(shape=[1], value=128, dtype='int64')
label = fluid.layers.fill_constant(shape=[1, 150], value=0.5, dtype='float32')
with fluid.device_guard("cpu"): #一行命令,指定该网络层运行设备为CPUemb = fluid.embedding(input=data, size=(10000000, 150), dtype='float32')
out = fluid.layers.l2_normalize(x=emb, axis=-1)cost = fluid.layers.square_error_cost(input=out, label=label)
avg_cost = fluid.layers.mean(cost)
sgd_optimizer = fluid.optimizer.SGD(learning_rate=0.001)
sgd_optimizer.minimize(avg_cost)place = fluid.CUDAPlace(0)
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())
result = exe.run(fluid.default_main_program(), fetch_list=[avg_cost])

因此,在显存有限时你可以参考上面的示例将一些计算密度不高的网络层设置在CPU上避免显存超出。

好处二:合理设置运行设备,减少数据传输

如果你在GPU模式下训练模型,希望提升训练速度,那么可以看看模型中是否存在一些不必要的数据传输。在文章开头我们提到CPU和GPU之间的数据拷贝是耗时的,因此如果能够避免这样的情况,就有可能提升模型的性能。

在下面的内容中,我们将教你如何通过profile工具分析数据传输开销,以及如何使用device_guard避免不必要的数据传输,从而提升模型性能。大致流程如下:

  1. 首先使用profile工具对模型进行分析,查看是否存在GpuMemcpySync的调用耗时。若存在,则进一步分析发生数据传输的原因。

  2. 通过Profiling Report找到发生GpuMemcpySync的OP。如果需要,可以通过打印log,找到GpuMemcpySync发生的具体位置。

  3. 尝试使用device_guard设置部分OP的运行设备,来减少GpuMemcpySync的调用。

  4. 最后比较修改前后模型的Profiling Report,或者其他用来衡量性能的指标,确认修改后是否带来了性能提升。

步骤1、使用profile工具确认是否发生了数据传输

首先我们需要分析模型中是否存在CPU和GPU之间的数据传输。在OP执行过程中,如果输入Tensor所在的设备与OP执行的设备不同,就会自动将输入Tensor从CPU拷贝到GPU,或者从GPU拷贝到CPU,这个过程是同步的数据拷贝,通常比较耗时。下列示例代码的14行设置了profile,利用profile工具我们可以看到模型的性能数据。

import paddle.fluid as fluid
import paddle.fluid.compiler as compiler
import paddle.fluid.profiler as profilerdata1 = fluid.layers.fill_constant(shape=[1, 3, 8, 8], value=0.5, dtype='float32')
data2 = fluid.layers.fill_constant(shape=[1, 3, 5, 5], value=0.5, dtype='float32')
shape = fluid.layers.shape(data2)
shape = fluid.layers.slice(shape, axes=[0], starts=[0], ends=[4])
out = fluid.layers.crop_tensor(data1, shape=shape)
place = fluid.CUDAPlace(0)
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())
compiled_prog = compiler.CompiledProgram(fluid.default_main_program())
with profiler.profiler('All', 'total') as prof:for i in range(10):result = exe.run(program=compiled_prog, fetch_list=[out])

在上述程序运行结束后,将会自动地打印出下面的Profiling Report,可以看到GpuMemCpy Summary中给出了2项数据传输的调用耗时。如果GpuMemCpy Summary中存在GpuMemcpySync,那么就说明你的模型中存在同步的数据拷贝。

进一步分析,可以看到slice和crop_tensor执行中都发生了GpuMemcpySync。我们通过查看网络的定义,就会发现尽管我们在程序中设置了GPU模式运行,但是shape这个OP将输出结果存放在CPU上,导致后面在GPU上执行的slice使用这个结果时发生了从CPU到GPU的数据拷贝。slice的输出结果存放在GPU上,而crop_tensor用到这个结果的参数默认是从CPU上取数据,因此又发生了一次数据拷贝。

------------------------->     Profiling Report     <-------------------------Note! This Report merge all thread info into one.
Place: All
Time unit: ms
Sorted by total time in descending order in the same threadTotal time: 26.6328Computation time       Total: 13.3133     Ratio: 49.9884%Framework overhead     Total: 13.3195     Ratio: 50.0116%-------------------------     GpuMemCpy Summary     -------------------------GpuMemcpy                Calls: 30          Total: 1.47508     Ratio: 5.5386%GpuMemcpyAsync         Calls: 10          Total: 0.443514    Ratio: 1.66529%GpuMemcpySync          Calls: 20          Total: 1.03157     Ratio: 3.87331%-------------------------       Event Summary       -------------------------Event                                                       Calls       Total       CPU Time (Ratio)        GPU Time (Ratio)        Min.        Max.        Ave.        Ratio.
FastThreadedSSAGraphExecutorPrepare                         10          9.16493     9.152509 (0.998645)     0.012417 (0.001355)     0.025192    8.85968     0.916493    0.344122
shape                                                       10          8.33057     8.330568 (1.000000)     0.000000 (0.000000)     0.030711    7.99849     0.833057    0.312793
fill_constant                                               20          4.06097     4.024522 (0.991025)     0.036449 (0.008975)     0.075087    0.888959    0.203049    0.15248
slice                                                       10          1.78033     1.750439 (0.983212)     0.029888 (0.016788)     0.148503    0.290851    0.178033    0.0668471
GpuMemcpySync:CPU->GPU                                      10          0.45524     0.446312 (0.980388)     0.008928 (0.019612)     0.039089    0.060694    0.045524    0.0170932
crop_tensor                                                 10          1.67658     1.620542 (0.966578)     0.056034 (0.033422)     0.143906    0.258776    0.167658    0.0629515
GpuMemcpySync:GPU->CPU                                      10          0.57633     0.552906 (0.959357)     0.023424 (0.040643)     0.050657    0.076322    0.057633    0.0216398
Fetch                                                       10          0.919361    0.895201 (0.973721)     0.024160 (0.026279)     0.082935    0.138122    0.0919361   0.0345199
GpuMemcpyAsync:GPU->CPU                                     10          0.443514    0.419354 (0.945526)     0.024160 (0.054474)     0.040639    0.059673    0.0443514   0.0166529
ScopeBufferedMonitor::post_local_exec_scopes_process        10          0.341999    0.341999 (1.000000)     0.000000 (0.000000)     0.028436    0.057134    0.0341999   0.0128413
eager_deletion                                              30          0.287236    0.287236 (1.000000)     0.000000 (0.000000)     0.005452    0.022696    0.00957453  0.010785
ScopeBufferedMonitor::pre_local_exec_scopes_process         10          0.047864    0.047864 (1.000000)     0.000000 (0.000000)     0.003668    0.011592    0.0047864   0.00179718
InitLocalVars                                               1           0.022981    0.022981 (1.000000)     0.000000 (0.000000)     0.022981    0.022981    0.022981    0.000862883

步骤2、通过log查看发生数据传输的具体位置

有时同一个OP会在模型中被用到很多次,例如可能我们会在网络的几个不同位置,都定义了slice层。这时候想要确认究竟是在哪个位置发生了数据传输,就需要去查看更加详细的调试信息,那么可以打印出运行时的log。依然以上述程序为例,执行GLOG_vmodule=operator=3 python test_case.py,会得到如下log信息,可以看到这两次数据传输:

  • 第3~7行log显示:shape输出的结果在CPU上,在slice运行时,shape的输出被拷贝到GPU上

  • 第9~10行log显示:slice执行完的结果在GPU上,当crop_tensor执行时,它会被拷贝到CPU上。

I0406 14:56:23.286592 17516 operator.cc:180] CUDAPlace(0) Op(shape), inputs:{Input[fill_constant_1.tmp_0:float[1, 3, 5, 5]({})]}, outputs:{Out[shape_0.tmp_0:int[4]({})]}.
I0406 14:56:23.286628 17516 eager_deletion_op_handle.cc:107] Erase variable fill_constant_1.tmp_0 on CUDAPlace(0)
I0406 14:56:23.286725 17516 operator.cc:1210] Transform Variable shape_0.tmp_0 from data_type[int]:data_layout[NCHW]:place[CPUPlace]:library_type[PLAIN] to data_type[int]:data_layout[ANY_LAYOUT]:place[CUDAPlace(0)]:library_type[PLAIN]
I0406 14:56:23.286763 17516 scope.cc:169] Create variable shape_0.tmp_0
I0406 14:56:23.286784 17516 data_device_transform.cc:21] DeviceTransform in, src_place CPUPlace dst_place: CUDAPlace(0)
I0406 14:56:23.286867 17516 tensor_util.cu:129] TensorCopySync 4 from CPUPlace to CUDAPlace(0)
I0406 14:56:23.287099 17516 operator.cc:180] CUDAPlace(0) Op(slice), inputs:{EndsTensor[], EndsTensorList[], Input[shape_0.tmp_0:int[4]({})], StartsTensor[], StartsTensorList[]}, outputs:{Out[slice_0.tmp_0:int[4]({})]}.
I0406 14:56:23.287140 17516 eager_deletion_op_handle.cc:107] Erase variable shape_0.tmp_0 on CUDAPlace(0)
I0406 14:56:23.287220 17516 tensor_util.cu:129] TensorCopySync 4 from CUDAPlace(0) to CPUPlace
I0406 14:56:23.287473 17516 operator.cc:180] CUDAPlace(0) Op(crop_tensor), inputs:{Offsets[], OffsetsTensor[], Shape[slice_0.tmp_0:int[4]({})], ShapeTensor[], X[fill_constant_0.tmp_0:float[1, 3, 8, 8]({})]}, outputs:{Out[crop_tensor_0.tmp_0:float[1, 3, 5, 5]({})]}.

步骤3、使用device_guard避免不必要的数据传输

在上面的例子中,shape输出的是一个1-D的Tensor,因此在slice执行时,计算代价相对于数据传输代价或许是更小的。如果将slice设置在CPU上运行,就可以避免2次数据传输,那么是不是有可能提升模型速度呢?我们尝试修改程序,将slice层设置在CPU上执行:

import paddle.fluid as fluid
import paddle.fluid.compiler as compiler
import paddle.fluid.profiler as profilerdata1 = fluid.layers.fill_constant(shape=[1, 3, 8, 8], value=0.5, dtype='float32')
data2 = fluid.layers.fill_constant(shape=[1, 3, 5, 5], value=0.5, dtype='float32')
shape = fluid.layers.shape(data2)
with fluid.device_guard("cpu"): # 一行命令,指定该网络层运行设备为CPUshape = fluid.layers.slice(shape, axes=[0], starts=[0], ends=[4])
out = fluid.layers.crop_tensor(data1, shape=shape)
place = fluid.CUDAPlace(0)
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())
compiled_prog = compiler.CompiledProgram(fluid.default_main_program())
with profiler.profiler('All', 'total') as prof:for i in range(10):result = exe.run(program=compiled_prog, fetch_list=[out])

步骤4、比较修改前后模型,确认是否带来性能提升

再次观察Profiling Report 中GpuMemCpy Summary的内容,可以看到GpuMemCpySync 这一项已经被消除了。同时注意到,下面的Total time为 14.5345 ms,而修改前是26.6328 ms,速度提升一倍!此实验说明使用device_guard避免数据传输后,示例模型的性能有了明显的提升。

在实际的模型中,若GpuMemCpySync调用耗时占比较大,并且可以通过设置device_guard避免,那么就能够带来一定的性能提升。

------------------------->     Profiling Report     <-------------------------Note! This Report merge all thread info into one.
Place: All
Time unit: ms
Sorted by total time in descending order in the same threadTotal time: 14.5345Computation time       Total: 4.47587     Ratio: 30.7948%Framework overhead     Total: 10.0586     Ratio: 69.2052%-------------------------     GpuMemCpy Summary     -------------------------GpuMemcpy                Calls: 10          Total: 0.457033    Ratio: 3.14447%GpuMemcpyAsync         Calls: 10          Total: 0.457033    Ratio: 3.14447%-------------------------       Event Summary       -------------------------Event                                                       Calls       Total       CPU Time (Ratio)        GPU Time (Ratio)        Min. Max.        Ave.        Ratio.
FastThreadedSSAGraphExecutorPrepare                         10    7.70113     7.689066 (0.998433)     0.012064 (0.001567)     0.032657    7.39363     0.770113    0.529852
fill_constant                                               20          2.62299     2.587022 (0.986287)     0.035968 (0.013713)     0.071097    0.342082    0.13115     0.180466
shape                                                       10          1.93504     1.935040 (1.000000)     0.000000 (0.000000)     0.026774    1.6016      0.193504    0.133134
Fetch                                                       10          0.880496    0.858512 (0.975032)     0.021984 (0.024968)     0.07392     0.140896    0.0880496   0.0605797
GpuMemcpyAsync:GPU->CPU                                     10        0.457033    0.435049 (0.951898)     0.021984 (0.048102)     0.037836    0.071424    0.0457033   0.0314447
crop_tensor                                                 10          0.705426    0.671506 (0.951916)     0.033920 (0.048084)     0.05841     0.123901    0.0705426   0.0485346
slice                                                       10          0.324241    0.324241 (1.000000)     0.000000 (0.000000)     0.024299    0.07213     0.0324241   0.0223084
eager_deletion                                              30          0.250524    0.250524 (1.000000)     0.000000 (0.000000)     0.004171    0.016235    0.0083508   0.0172365
ScopeBufferedMonitor::post_local_exec_scopes_process        10   0.047794    0.047794 (1.000000)     0.000000 (0.000000)     0.003344    0.014131    0.0047794   0.00328831
InitLocalVars                                               1           0.034629    0.034629 (1.000000)     0.000000 (0.000000)     0.034629    0.034629    0.034629    0.00238254
ScopeBufferedMonitor::pre_local_exec_scopes_process         10   0.032231    0.032231 (1.000000)     0.000000 (0.000000)     0.002952    0.004076    0.0032231   0.00221755

总结

通过以上实验对比可以发现,device_guard接口能够做到一条命令即可合理设置模型网络层的运行设备,对模型进行GPU和CPU计算的更灵活调度,将服务器的资源利用到极致,解决显存容量捉襟见肘导致模型无法训练的问题。怎么样,这个功能是不是相当实用!心动不如心动,快快参考本文的方法,尽情训练自己的模型吧!

如在使用过程中有问题,可加入飞桨官方推理部署QQ群:696965088

官网地址:

https://www.paddlepaddle.org.cn

飞桨开源框架项目地址:

GitHub:

https://github.com/PaddlePaddle/Paddle

Gitee:

https://gitee.com/paddlepaddle/Paddle

END

模型训练太慢?显存不够?这个方法让你的GPU联手CPU相关推荐

  1. 模型训练慢和显存不够怎么办?GPU加速混合精度训练

    目录 混合精度训练 理论原理 三大深度学习框架的打开方式 Pytorch Tensorflow PaddlePaddle 混合精度训练 一切还要从2018年ICLR的一篇论文说起... <MIX ...

  2. 解决模型训练过程中显存不释放,不断累加导致爆显存

    问题:在模型训练的过程中,自己手动加入了每隔2000轮测试一下模型在测试集上的效果,结果24G的显存每次到4000轮也就是测试两次就爆掉显存了,后来通过验证找到了问题,并且在测试代码段最后加入torc ...

  3. 浅谈深度学习:如何计算模型以及中间变量的显存占用大小

    原文链接:https://oldpan.me/archives/how-to-calculate-gpu-memory 前言 亲,显存炸了,你的显卡快冒烟了! torch.FatalError: cu ...

  4. 模型训练太慢?显存不够用?这个算法让你的GPU老树开新花

    一只小狐狸带你解锁NLP/ML/DL秘籍 作者:小鹿鹿鹿,夕小瑶 老板,咱们就一台Titan Xp,训不动BERT呀 没钱买机器,自己想办法. 委屈T^T 我听说混合精度训练可以从算法上缓解这个问题? ...

  5. 【半精度】Pytorch模型加速和减少显存

    如标题所示,这是PyTorch框架提供的一个方便好用的trick:开启半精度.直接可以加快运行速度.减少GPU占用,并且只有不明显的accuracy损失. 之前做硬件加速的时候,尝试过多种精度的权重和 ...

  6. 游戏显示计算机空间不足,打游戏显示“显存不足”怎么解决?图解显存不足的方法步骤...

    玩大型游戏的时候出现显卡内存不足的提示,根本无法进行游戏,那该怎么办呢?这种情况大多出现在玩运行大型游戏或者运行3D室内软件之类.下面,就随小编一起看看该问题的解决方法. 懂电脑的朋友都知道显卡是我们 ...

  7. 阿里 NIPS 2017 Workshop 论文:基于 TensorFlow 的深度模型训练 GPU 显存优化

    NIPS 2017 在美国长滩举办,场面非常热烈.阿里巴巴一篇介绍深度模型训练 GPU 显存优化的论文<Training Deeper Models by GPU Memory Optimiza ...

  8. 阿里NIPS 2017论文解读:如何降低TensorFlow训练的显存消耗?

    阿里妹导读:被誉为国际顶级的机器学习和神经网络学术会议NIPS 2017于12月4日-9日在美国加州长滩市举行.在本届会议上,阿里巴巴除有两篇论文入选Workshop并进行Oral和Poster形式报 ...

  9. AI 绘画(2):Ai模型训练,Embedding模型,实现“人物模型“自由

    文章目录 文章回顾 感谢人员 题外话 Ai绘画公约 Ai模型训练 硬件要求 显存设置 查看显存大小 显存过小解决方法 视频教程 前期准备 SD配置设置 SD设置配置 SD训练配置pt生成 训练集收集 ...

最新文章

  1. 三层交换机环境的上网行为管理方案
  2. OpenCASCADE绘制测试线束:几何命令之近似值
  3. java class _Java Class文件详解
  4. linux vi-vim编辑器快捷键
  5. oracle日期处理函数
  6. MySQL中的事务及读写锁实现并发访问控制
  7. Deep Speaker代码解析
  8. IDEA给项目添加lib/jar
  9. Black Hat 2017:不容错过的七大主题演讲
  10. Freeswitch服务+语音网关设备发送短信功能
  11. 自媒体怎么同时多平台发文章?5分钟发到30+自媒体平台上
  12. For Your Dream
  13. “湖南索御文化传媒”新闻发布会即将启动
  14. numpy取数组中的行和列
  15. Android学习网址大全
  16. Apache安装和配置详细
  17. keyshot聚光灯_KeyShot 6使用技巧
  18. 中英文名片拼写法对照
  19. 这几款小众软件,电脑常备
  20. 你知道Kafka和Redis的各自优缺点吗?一文带你优化选择,不走弯路

热门文章

  1. 树莓派ARM汇编语言编程十讲(第1讲)
  2. day01-2021-08-23
  3. TensorRT+ int8官方论坛中有趣的讨论总结
  4. ios上拉下划出现黑色背景的解决办法
  5. Pr学习DAY1-----认识Pr
  6. 二手手机回收为何会涌入大批正规军,它们如何从中牟利?
  7. php图片怎么存入指定文件夹_在PHP中如何把图片存入数据库和文件夹里
  8. mysql导入数据报错 Errcode: 28 “No space left on device“
  9. mysql插入数据创建触发器填充uuid字段值
  10. access查询mysql_Access查询之自定义字段