news 2026/10/23 5:18:01

大模型分布式训练:并行策略、通信优化与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型分布式训练:并行策略、通信优化与工程实践

如果你亲手训过大模型,一定经历过那种半夜盯屏的焦虑:显存又爆了,显卡利用率忽高忽低,日志里NCCL直接给你一个超时错误。单张H100的显存和算力在普通场景下已经很强,但放到7B、13B甚至70B参数面前,真的就像一个水缸接游泳池。分布式训练成为大模型基础理论里的必修课,不是因为它是新潮概念,而是因为单卡方案在物理上就走不通。

这篇文章会把分布式训练拆成几个容易理解的问题:为什么需要多卡、不同并行策略分别解决什么、通信为什么是隐形瓶颈、实际工程里怎么落地和排障。适合刚接触大模型训练、准备用服务器或者云端多卡跑模型的同学。内容以原理和实操经验为主,代码只给最小闭环,具体框架和库的版本我会尽量说清,避免你踩我踩过的坑。

1. 分布式训练为什么是必修课

1.1 单卡训练的物理天花板

先做个简单的账。用常见的7B模型举例,参数量是7×10^9,用FP16存储权重就要约14GB。就算把这14GB权重塞进一张80GB的卡,训练过程中还需要梯度、优化器状态和激活值。以AdamW优化器为例,它会为每个参数保存两份FP32状态,momentum和variance,两份状态加起来是56GB。梯度即使只保留BF16也要14GB,FP32就是28GB。还没算激活值。所以一个7B模型,仅仅模型、梯度和优化器状态就到了84GB到98GB。80GB的A100/H100单卡,光这一关就过不去。更不用说反向传播时每一层的激活值还要按batch size和序列长度成倍往外扩。

算力同样有天花板。单卡标称FLOPs再高,受显存带宽和功耗限制,实际吞吐很难线性压满。7B模型的训练时间通常以天甚至周为单位,一次实验跑完再复盘,反馈周期太长。分布式训练把时间和空间成本摊到多张卡上,才让“多试几次”变成可能。注意一个容易被忽略的点:即使模型单卡能放下,如果训练数据量大、迭代速度跟不上,多卡并行也能明显缩短实验周期。所以分布式训练并不是大模型专属,只是一个需求程度的问题。

1.2 分布式训练到底在分配什么

分布式训练表面上是在“用更多GPU”,本质上是在同时分配三种资源:算力、显存、通信带宽。算力不够就加卡,显存不够就切模型或offload,通信带宽不够就需要优化并行方式和同步策略。这三者耦合得非常紧。数据并行把算力平均分到每张卡,但每张卡仍然要存完整模型,所以它没有解决显存问题。张量并行、流水线并行把模型切分到不同卡上,解决了显存,但每层前向和反向都可能产生通信。所以不存在一种万能并行方案,只能根据模型结构和硬件拓扑组合着用。

这也能解释为什么大厂的技术报告里训练方案总是很复杂,比如“8路张量并行+8路流水线并行+64路数据并行”。这句话的本质是把模型和计算做了一次三维切分:张量并行解决单层太大,流水线并行解决层数太多,数据并行解决数据和吞吐不够。理想情况下,三套策略各管一摊,再通过调度和通信把它们拼起来。

理解这些资源分配,是后续所有调优的起点。很多人一上多卡就急着改代码,结果发现GPU利用率只有30%,其实就是没搞清楚瓶颈在算力、显存还是通信。我自己的习惯是:任何训练任务开始前,先画一张“资源表”,标出模型参数在每张卡上怎么放、每步迭代有哪些通信、通信的数据量有多大。这张表比任何监控工具都能更快定位问题。

2. 核心并行策略:数据并行、模型并行与流水线并行

2.1 数据并行:最简单的多卡复制方案

数据并行(Data Parallelism,DP)是大模型训练里最简单、也是大多数人最早接触的并行方式。它的思路非常朴素:每张卡上都放一份完整的模型参数,把训练数据按卡数切分,每张卡用自己的那部分算前向、算梯度,最后通过集合通信把梯度平均,再统一更新参数。这样4张卡训练1000个batch,就等效于单卡训练250个更大的batch(不考虑通信开销)。PyTorch里的DDP(DistributedDataParallel)就是干这件事最主流的方案。

它解决的是算力不足,而不是显存不足。因为模型在每个副本上必须完整放得下,2卡数据并行并不会让单卡能塞进13B模型。如果模型已经大到单卡放不下,数据并行需要先配合下面要讲的模型并行或ZeRO才有意义。

DDP的实现细节对很多人来说是个黑盒。PyTorch里它默认用NCCL做梯度的All-Reduce,而且不是等所有梯度都算完才开始通信,而是一边反传一边把准备好的梯度打包,分桶通信。这个设计叫梯度桶(gradient bucket),好处是通信和计算可以重叠一部分,能把All-Reduce的等待时间藏进反向传播里。默认桶大小是25MB左右,DDP里可以通过bucket_cap_mb参数调整。我把这个参数当做一个旋钮:当小batch导致通信占比过高时,把桶调大一点,减少通信次数;当显存吃紧时调小一点,减少通信峰值显存。

2.2 张量并行:单层模型也能拆开算

张量并行(Tensor Parallelism)也叫层内模型并行,是把同一个Transformer层里的矩阵乘法拆开,放到多张卡上并行算。比如一个线性层Y = XW,如果按输出维度切W,得到W1和W2两份,每张卡分别算XW1和XW2,再对Y做一次All-Reduce,就得到了完整输出。Megatron-LM的核心做法就是把Attention和MLP里的矩阵按列切、按行切组合起来,让通信次数保持在很小的范围内。

这种做法能显著降低单卡显存,因为每一层参数只存了1/N。但它对通信延迟非常敏感,因为每一次Transformer前向都会引入两次以上的All-Reduce,反向还要配套做Reduce-Scatter。所以张量并行最好在单节点内使用,依赖NVLink/NVSwitch的高速P2P,跨节点带宽不够的话,通信延迟会直接把计算优势吃掉。很多大厂训练超大规模模型时,节点内用张量并行,节点间再用数据并行或流水线并行,这是很常见的组合。

实操中还有一个容易踩的坑:张量并行的切分维度必须和模型代码严格匹配。比如QKV投影的列切、输出投影的行切,任何一个维度搞反,模型可能还能跑,但loss曲线会出现诡异的波动。第一次实现时最好用一个很小的模型做数值对比,确保张量并行的输出和单卡输出在误差范围内一致。

2.3 流水线并行:把网络按层切成一条产线

流水线并行则是把模型的层按顺序切给不同设备。比如一个12层Transformer被切成4段,卡0跑第1-3层,卡1跑第4-6层……每张卡只保存自己这几层的参数和激活值。前向时,数据像工厂流水线一样从第一段往后传。这种方案通信量远远小于张量并行,因为卡间只需要传中间激活值,不需要反复All-Reduce。

但流水线并行有个固有的问题叫气泡(bubble)。类比工厂流水线:四道工序,开工前只有第一道工位在干活,后面都在等;收尾时又倒过来。如果只把一个batch切成一个样本,整个训练周期里大量时间在等待。通常解法是micro-batch切分和1F1B调度,把一个大的batch再切成很多小份,交错进入流水线,把气泡填平。GPipe提出的是同一时间所有stage处理不同micro-batch,而1F1B能进一步减小显存占用。

实操中的经验是:micro-batch数量最好至少是流水线stage数量的2-4倍,且流水线深度不要盲目拉大,stage太多会导致每段计算太少、传输和调度的开销占比变高。我自己试过一个极端案例:把8层的模型切成8个stage,每张卡只有一层,结果网络大部分时间在等通信,吞吐反而不如单卡。流水线并行适合层数足够多、单个stage计算量足够大的场景。

2.4 ZeRO:DeepSpeed 对显存的系统性压缩

再来看数据并行在显存上的进化。DeepSpeed提出的ZeRO(Zero Redundancy Optimizer)思想,简单说是把模型训练态里大量重复保存的东西从每卡全量改成全局分片。它分成三个阶段:ZeRO-1分片优化器状态,ZeRO-2分片梯度和优化器状态,ZeRO-3把参数、梯度、优化器状态全部做分片。通信方式也随之从All-Reduce变成Reduce-Scatter加All-Gather。

为什么这么改?数据并行里每张卡存一份完整参数和优化器状态是冗余的。优化器状态占大头,尤其是Adam,状态体积是参数体积的数倍。ZeRO每次迭代先把本卡分片的梯度reduce-scatter,得到分片梯度后更新分片优化器状态,等到下一次前向前才把参数all-gather出来用。这样通信总量没有变大,但显存被压缩到接近1/N,代价是多了几次通信。

ZeRO-3配合Offload到CPU或者NVMe,还能继续把显存门槛再降一个量级,代价是PCIe带宽很慢,吞吐会明显下降。所以它适合“先让模型能跑起来”的场景,如果追求极致性能,还是要配合张量并行和高效通信来用。我用ZeRO-3跑过一个13B模型,单卡40GB的A100也能载入,但训练速度相比纯DDP下降了不少。后来改成“ZeRO-2 + 小batch + 梯度累积”,速度和稳定性反而更好。

2.5 实际策略怎么组合

没有哪个策略是银弹。常见组合方式:模型能单卡放下时用数据并行;单卡放不下时,先加ZeRO-3;节点内用张量并行解决最大层放不下的问题;跨节点用流水线并行降低通信;数据并行在所有组合里都要有,用来吃满数据。

并行方式显存怎么分主要通信瓶颈典型场景
数据并行每卡完整副本梯度All-Reduce通信带宽模型能单卡装下
ZeRO状态分片Reduce-Scatter+All-Gather多次聚合通信单卡放不下,以装下为主
张量并行层内参数切分每层多次All-ReduceNVLink带宽大单层,节点内
流水线并行按层切分激活值传输气泡和调度多层结构,跨节点

这张表是训练开始前做决策的起点。如果模型最大的单层矩阵都能塞进一张卡,一般不需要张量并行;如果层数很多、且每层参数量不大,流水线并行更划算;如果只是想提升吞吐,数据并行/ZeRO永远是最省事的起点。

3. 通信和集群:分布式训练真正难的地方

3.1 All-Reduce 和 NCCL:梯度同步是怎么发生的

如果只用一句话解释分布式训练的难点,我会说难点在通信,不在计算。很多第一次上多卡的人以为模型并行能跑得飞快,结果发现通信成了拖油瓶。原因在于训练过程中,每张卡算完梯度后必须做集合通信,把不同卡上的梯度对齐,这本身就要求同步。经典的Ring All-Reduce把数据切成N块,在卡间环形传递累加,再传一轮把结果同步给所有人。通信时间近似等于2×(N-1)/N×数据量×通信次数/带宽,规模越大、数据量越大,通信占比越高。

实际工程里,NCCL是英伟达的集合通信库,几乎所有框架的分布式训练底层都走它。NCCL会根据节点拓扑自动选择算法,可能是ring、tree或者nvls。遇到卡死时NCCL_DEBUG=INFO是一种排障方式,但不建议长期开,日志量太大。可以先确认多卡是否真的通过P2P可达,再用官方nccl-tests测一下带宽,能够判断瓶颈是在网络、驱动还是拓扑识别。

这里有一个常见的认知误区:很多人以为All-Reduce只在更新前做一次,实际上反向传播过程中DDP会分段多次通信。如果模型很小、梯度很小,通信次数反而比数据量更影响延迟。所以小模型跑多卡时,经常出现加速比很差的现象——不是带宽不够,而是每次通信的延迟压不下来。

3.2 网络、NVLink 与 RDMA:硬件选型直接决定上限

通信对网络的依赖非常大。一台服务器里的8块GPU通常两两之间有NVLink,NVLink带宽在A100上约600GB/s(双向),比PCIe 4.0的32GB/s高了一个数量级,而且NVLink是多卡全互联,所以张量并行必须留在节点内。跨节点通信则常见用InfiniBand或RoCE,单端口常见200Gb/s或400Gb/s,换算下来只有25-50GB/s,和NVLink显然不是一个量级。

所以分布式集群最理想的拓扑是:节点内NVLink全互联,节点间高速RDMA网络。实际训练时,如果你发现节点内通信和节点间通信混在一起,NCCL会自动按拓扑分层执行All-Reduce,前提是网络配置正确。很多人买了几台带公网IP的服务器就打算做分布式训练,结果跨节点通信走的是普通TCP,带宽只有几百MB/s,训练速度惨不忍睹。跨节点训练尽量选择云厂商的RDMA网卡或者自建IB集群,至少也要有RoCE。

选型时还有一个容易被忽视的点:通信对稳定性要求极高。RDMA网卡的固件版本、驱动的ibstat状态、交换机的丢包策略,任何一个出问题都可能让训练频繁超时。我的经验是:机器到位后,先跑一轮完整的nccl-tests,用allreduce -b 128M -f 8这种压测命令看看多卡带宽是否符合预期,再开始训练。

3.3 如何估算通信成本

我们可以粗算一笔账。假设一个7B模型,每次迭代要All-Reduce一次梯度,梯度大小为7B×2字节(BF16)约14GB,All-Reduce实际通信量至少要这个数据量的两倍左右,也就是约28GB。如果跑在200Gbps的RDMA网络上,理论带宽25GB/s,单次通信就超过1秒。如果模型计算本身每步只有0.2秒,多卡同步会直接把单步时间拉到1秒以上。所以分布式训练必须调大batch size,让计算和通信重叠起来,否则数据并行几乎没法规模化。

这也是为什么很多人说小batch跑多卡不如单卡快。通信和计算需要重叠,只有当计算时间明显大于通信时间的时候,多卡优势才体现出来。一种务实的做法是:先量一下单卡跑一个micro-batch需要多久,再估算通信时间,如果通信占比超过20%,就要考虑增大batch、合并通信或者换更快的网络。

通信成本不是一个固定的数,它会随卡数增加而增加。Ring All-Reduce的优点在于带宽不随卡数明显下降,但延迟会随规模增长。所以那些号称“千卡线性扩展”的训练系统,绝大多数都用了很大的batch,同时把网络基础设施做到了极致。普通用户想复现这种效果,与其纠结算法,不如先把网络带宽和拓扑优化好。

4. 可落地的分布式训练实操笔记

4.1 PyTorch DDP 最小闭环

直接给一份最简代码骨架,里面包含分布式训练最容易漏掉的几个点:初始化进程组、DistributedSampler、set_device、使用rank隔离日志。

import os import torch import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP from torch.utils.data import DataLoader, DistributedSampler def main(): dist.init_process_group(backend="nccl") rank = dist.get_rank() world_size = dist.get_world_size() torch.cuda.set_device(rank) model = torch.nn.Transformer(d_model=512, nhead=8, num_encoder_layers=6).to(rank) model = DDP(model, device_ids=[rank]) dataset = [...] # 你自己的数据 sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank, shuffle=True) loader = DataLoader(dataset, batch_size=8, sampler=sampler, num_workers=4, pin_memory=True) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4) for epoch in range(3): sampler.set_epoch(epoch) for x, y in loader: x = x.to(rank) y = y.to(rank) optimizer.zero_grad() loss = model(x, y) loss.backward() optimizer.step() if rank == 0 and step % 10 == 0: print(f"epoch {epoch} step {step} loss {loss.item()}")

启动命令用torchrun,比如单机8卡:

torchrun --nproc_per_node=8 train.py

多机场景需要设置MASTER_ADDR和MASTER_PORT,每个节点上的进程数保持一致。注意DistributedSampler的set_epoch一定要做,否则每个epoch的shuffle都一样,模型会学到数据的顺序偏差。pin_memory=True配合num_workers>0对数据加载速度提升非常明显,尤其是数据读入频繁训练的时候。

4.2 混合精度与梯度累加的正确姿势

混合精度训练就是把部分张量用FP16/BF16存,梯度用FP32主权重更新。FP16要用GradScaler防止梯度下溢,BF16因为没有范围过小的问题,省去了loss scaling,在A100/H100上已经成为默认选择。一开始先做纯FP16可能因为下溢导致loss不下降,换到BF16后稳定很多。注意混合精度并不是只把模型半精度化,而是要保持一个FP32主权重副本,相当于额外内存。

梯度累加是另一个常见操作。当单卡放不下per_gpu_batch_size太大时,把多个小step的梯度累加后做一次更新。要清楚global_batch_size = per_gpu_batch_size × 卡数 × gradient_accumulation_steps。显存不够时优先减小per_gpu_batch_size,再用梯度累加补回来,两者对训练效果的影响并不完全等价,因为BN和某些算子对batch内的样本统计敏感,Transformer类模型通常还好。

踩坑记录:混合精度+梯度累加会让很多人掉进一个隐蔽的坑。正确做法是只在累计到指定步数后调用scaler.step和scaler.update,而不是在每个micro-step里都更新scale。否则缩放因子会波动得非常厉害,loss曲线会出现莫名其妙的尖峰。我的习惯是写一个自定义loop,用一个accum_steps计数器控制,只有当前step刚好整除accum_steps时才做optimizer.step()。

4.3 多卡环境下的全局batch和超参调节

多卡带来的一个隐形问题是总batch变大。以8卡为例,单卡batch 8时总batch为64,如果还想保持总batch等于单卡时代的64,那么每卡要降到1,卡多但没有意义;所以通常会增加训练batch。大batch会提高吞吐,但过大时收敛会变慢甚至不收敛。经验做法是先用1/8或1/16的小batch跑通一个step,再逐步放大,观察训练loss曲线。学习率方面,Adam类优化器通常要在增大batch时提高learning rate或延长warmup,具体可以用linear scaling rule作为起点,然后微调。

实际调参中我建议把“训练吞吐”和“收敛步数”分开看待。吞吐是硬件指标,收敛步数是算法指标。多卡训练的目标应该是在ble步数增加不多的情况下,提高每秒吞吐。如果放大batch 8倍,但收敛需要的步数只增加了20%,那整体收益就非常明显。如果batch放大后收敛步数翻倍,即使吞吐翻倍,总时间可能也没省多少。

4.4 冒烟测试:上全规模前必做

真正大规模训练前,我强烈建议先做一次小规模冒烟测试。用1/100数据、2-4张卡、跑几十步,确认loss能正常下降、通信没有报错、checkpoint能正常保存加载,再上全量。很多人直接拿几百卡训练,结果因为一个dataloader的bug浪费整个调试时间。分布式训练排查成本高,小规模冒烟测试是最省时间的保险。

冒烟测试时还要验证一件事:不同rank上的模型初始权重必须完全一致。如果每个进程用自己的默认随机状态甚至自己加载了不同ckpt,loss曲线会乱跳到没法收敛。DDP本身会在初始化时同步参数,但如果你在构建模型后加了一些自定义初始化,或者用了torch.load加载权重,一定要确认所有rank都经历同样的操作。最简单的方法是用固定随机种子,并在每次加载ckpt后调用model.broadcast或sync_module_buffers。

4.5 一个前沿方向:MoE 与专家并行

再提一个和分布式强相关的方向:混合专家MoE。MoE模型里有很多专家网络,同一个token只激活其中一小部分专家,训练时通常把不同专家放在不同卡上,这种并行叫专家并行。它的问题是负载不均衡:热门专家可能会收到过多token,所以又要用到路由策略和负载均衡loss。在DeepSpeed和Megatron里都有MoE实现,通信模式从Dense模型的All-Reduce变成了All-to-All,对网络的要求更高。

这也是大模型分布式训练里比较前沿的玩法。如果你要做MoE,最好先跑通Dense模型的分布式训练,再叠加专家并行,否则很多通信错误会让你分不清是并行框架的问题还是MoE路由的问题。

5. 常见问题与排查实录

5.1 通信超时、NCCL 卡死

现象:单机多卡或跨机训练刚开始几分钟就卡住,日志停在某个Nccl... TIMEOUT。原因组合:多机时MASTER_ADDR填错;防火墙没放行;网卡驱动或NCCL版本不一致;共享文件系统读写锁异常;单机上P2P访问冲突。

常见处理步骤:先把多机网络是否互通确认掉,最简单的ping和nccl-tests最小case能跑通,再上训练。打开NCCL_DEBUG=INFO看它选用的网络类型,如果是Socket乱走说明IB没被识别。如果IB驱动有问题,可以先NCCL_IB_DISABLE=1走TCP验证,再修RDMA配置。

另一个容易被忽略的地方是时间同步。多机训练一定确保各节点的系统时间偏差不大,NCCL的握手阶段如果时间差太大,可能超时。还有,不要用NFS直接作为checkpoint的共享目录,某条路径权限不对,会卡住所有进程,看起来像NCCL挂死,其实是文件锁问题。先把checkpoint写到本地,再异步同步到共享存储,能省掉很多麻烦。

5.2 显存 OOM 怎么定位

现象:torch.cuda.OutOfMemoryError。先分清楚是哪个阶段:加载权重时就爆,说明模型参数+优化器状态已超显存;训练几步后才爆,一般是激活值。这个区分非常关键,因为解决方案完全不同。

如果是权重阶段就超,需要启动ZeRO、张量并行或流水线并行把参数和状态切到多卡上;如果是激活值阶段,降低单卡batch size是最直接手段,或者开启gradient checkpointing,把前向中间激活值通过重计算换显存,通常增加20-40%的计算时间但能降低很多显存;还可以调整padding策略,长文本样例尽量按长度分组,减少无效计算和显存浪费。

我通常用torch.cuda.max_memory_allocated()和torch.cuda.memory_summary()记录峰值显存出现在哪个阶段。如果发现某个张量特别大,可以通过hook查看前向里每个模块的活跃缓冲。这一步虽然繁琐,但能帮你在不盲目降batch的情况下找到真正的大头。

5.3 多卡加速比上不去的排查

现象:8卡训练,吞吐只有单卡的3-4倍。原因第一是batch太小,通信没有充分重叠;第二是DataLoader太慢,GPU等数据;第三是全卡All-Reduce频繁,梯度桶参数不合适;第四是负载不均,比如数据长度方差太大。

排查可以用nvidia-smi监视GPU利用率是否稳定在90%以上,同时看CPU和磁盘IO。优化方向:增大batch_size或gradient_accumulation_steps;DataLoader换成num_workers+pin_memory;开启DDP的bucket_cap_mb调整;使用文本长度分组和数据预切分。

症状常见原因优先排查
NCCL超时卡住多机网络不通/IB未识别ping、nccl-tests、NCCL_DEBUG
显存OOM权重和优化器/激活值超限看加载阶段 vs 训练阶段
吞吐低batch过小/数据加载慢/通信重叠差nvidia-smi + 调整scheduler

最后说个我自己的习惯:训练前先跑一个只有几十步的短任务,把所有warning都当error处理。分布式训练最怕的不是显存不够,而是通信和数据处理在暗处拖后腿。先把最小闭环跑稳,再往里面加Batch、加卡数、加优化技巧,每一步都留一个可回退的checkpoint。这样即使出问题,也不会浪费一整轮大规模训练的时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 5:17:25

Python中可变对象与不可变对象详细教程

前言 "可变"与"不可变"是理解 Python 数据模型的第一道坎,也是最容易产生诡异 bug 的地方:明明只是把列表传进函数"改一下",函数外面的列表却也跟着变了;明明写了 a b 然后改 a,b 却纹…

作者头像 李华
网站建设 2026/10/23 5:17:13

Python中函数的创建及调用

前言 函数在 Python 里有两种诞生方式:用 def 起名创建,或用 lambda 匿名创建。创建之后,它就是一个普通的对象——可以赋值、可以放进列表、可以当参数传给别的函数、也可以被函数返回。调用则更丰富:按位置调、按关键字调、用 *…

作者头像 李华
网站建设 2026/10/18 2:05:58

COMSOL声学模型超声无损检测仿真:核心思路与完整建模

从事超声无损检测仿真这几年,我越来越觉得COMSOL是最适合把这个领域“讲清楚”的工具。声学模型看着门槛不高,但真要拿它复现一次超声波探伤过程、算出A扫描信号,中间涉及物理场耦合、网格、时间步长和边界吸收的讲究特别多。今天我就把COMSO…

作者头像 李华
网站建设 2026/10/18 20:58:06

PostgresML 自托管完全指南:从 APT 安装到 GPU 加速与 pgvector 实战

后端人工智能机器学习RAG向量数据库 【免费下载链接】postgresml Postgres with GPUs for ML/AI apps. 项目地址: https://gitcode.com/gh_mirrors/po/postgresml 点击查看 免费下载 PostgresML 本质上是 PostgreSQL 的一个扩展,因此自托管 PostgresML …

作者头像 李华
网站建设 2026/10/18 20:58:08

OpenEuler Shell脚本起步:核心语法与调试排错实战

刚接触 OpenEuler 的 Shell 编程时,很多人觉得脚本不过就是把几条 Linux 命令按顺序写进一个文件,然后执行一下,仅此而已。这个理解不算错,但真正动手之后你会发现,一个看起来再简单的起步脚本,里面也藏着解…

作者头像 李华
网站建设 2026/10/18 20:58:07

前缀和与哈希表:LeetCode 560题和为K的子数组最优解

LeetCode Hot 100 里的 560 题“和为 K 的子数组”,我前前后后刷了三遍,每次都有新的理解。这道题在面试和算法学习中出现的频率极高,看起来只是“数一数有多少个连续子数组的和等于 k”,可真正把它讲透的人不多。很多解法上来就丢…

作者头像 李华