news 2026/8/31 12:40:52

GPU云的技术拐点:从调度优化到精细化运营的工程账

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GPU云的技术拐点:从调度优化到精细化运营的工程账

CoreWeave 在 AI 算力圈里经常被看成“GPU 云打工人”:上游要采购高价 GPU,下游要面对波动极大的训练和推理需求。所谓“拐点”,从工程视角看,并不是某一笔订单或新闻事件推动的,而是算力调度、集群利用率、网络架构和成本控制同时在往良性方向走。这篇内容不评价股价,而是从开发者视角拆解 GPU 云服务商从重资产压力转向精细化运营背后的技术账,同时给出一套租用 GPU 算力时的工程验证清单。无论你是算法工程师、平台工程师,还是刚准备用 GPU 云跑大模型的人,都可以按这条路径检查自己的环境和任务设计。

1. 为什么 GPU 云厂商会被看成“苦命打工人”

1.1 重资产模式:买卡、上架、折旧

GPU 云厂商最直接的资产就是 GPU。一张主流训练卡的价格不低,一台八卡服务器整机加上 CPU、内存、SSD、网卡、电源和散热改造后,成本会更高。这些硬件有明确的折旧周期,三五年后账面价值持续下降。与此同时,硬件要放在机房,占机柜、耗电、散热,还伴随搬迁、扩容、维修等人力成本。整个结构很像“打工人”的收入表:固定成本先压在身前,算力收入是在后面慢慢收回来的。

从运营角度看,GPU 云厂商的盈利改善主要来自两个变量:单卡单位成本下降和单卡产出提升。单卡单位成本下降靠规模采购、液冷降低散热电费、提高服务器密度;单卡产出提升靠调度器把空闲卡填满,靠客户工作负载更持久稳定。所以“拐点”不是营销词,而是资产周转和利用率同时改善的结果。

这个逻辑在团队内部同样存在。如果企业内部搭建 GPU 平台,只求“能申请到卡”,却不管任务结束后的资源回收,那后进来的任务就只能排队。很多内部平台排队时间很长,不是卡太少,而是缺少细粒度的资源回收机制:任务异常退出后,GPU 没有被释放;定时任务重叠提交,把资源池占满。

1.2 需求波动导致“卡闲下来就是亏”

训练任务一般有明显波峰波谷。白天算法工程师做实验,深夜批量跑数据;大模型训练任务又需要连续运行数天。如果资源池按峰值需求购买,那波谷时的 GPU 就处于闲置状态,而固定成本并没有减少。闲置的卡不像普通 CPU 服务器那样可以通过降低频率减少电费,它仍然在占机柜、折旧和承担维护成本。

云厂商解决这个问题的手段是混合售卖:一部分容量通过按需实例满足稳定需求,一部分通过竞价或可抢占实例吸收波动任务,还有一部分通过预留容量锁定长期订单。对用户而言,选择哪种售卖方式会直接影响成本。对厂商而言,这种组合能抬高整体利用率。

这个问题的技术本质是资源调度。如果调度器只能按整卡分配,那么一个小任务占一张大卡,显存和算力都浪费。于是出现了 GPU 分片、时间片、MIG(Multi-Instance GPU)、显存隔离等技术。这些手段的目标都一样:让每一块 GPU 的闲置资源尽可能少。对开发者来说,理解自己的任务适合独占整卡还是共享分片,直接影响排队时间和执行成本。

1.3 拐点背后的三个技术信号

判断一家 GPU 云服务商是否真正进入良性循环,可以观察三个工程指标:

  • 集群平均 GPU 利用率是否长期稳定,而不是只在压测时好看。
  • 任务排队时间是否缩短,说明调度策略在优化,而不是单纯加机器。
  • 同规格实例的价格和性能是否达到合理区间,说明成本控制传导到了用户侧。

这三个指标和开发者直接相关。你要在云上跑训练,不只要看单卡型号,还要看集群网络、调度排队时间、数据加载吞吐和故障恢复能力。这些因素对训练总时长的影响,往往比 GPU 峰值算力更大。

指标含义开发者如何观察
GPU 利用率GPU 计算单元忙闲比例nvidia-smi、DCGM 指标
排队等待时间资源申请到调度成功的时间kubectl describe pod中的 Events
实例性价比单位算力价格和性能表现用相同代码在不同实例上对比耗时
故障恢复节点故障后任务能否自动拉起检查工作负载的重试策略和 checkpoint

2. 支撑盈利拐点的技术底座

2.1 Kubernetes 是资源池的调度骨架

现代 GPU 云大多基于 Kubernetes 管理异构资源。用户提交一个包含 GPU 资源的 Pod,调度器根据节点上的可分配资源决定放在哪台机器。为了让 Kubernetes 感知 GPU,通常需要 NVIDIA Device Plugin。它把 GPU 以 Extended Resource 的形式上报为nvidia.com/gpu,调度器才能识别。

这个机制有两个关键点:

  • 默认调度器只能看到 GPU 数量,看不到显存、带宽、多卡拓扑等细节,需要额外插件或调度策略。
  • 如果没有 Device Plugin,Pod 就算设置了resources.limits,也不会被分配到 GPU,节点上的资源列表里根本没有这一项。

从一个最小工作负载看:

apiVersion: v1 kind: Pod metadata: name: gpu-test spec: restartPolicy: OnFailure containers: - name: cuda-test image: nvidia/cuda:12.4.1-base-ubuntu22.04 command: - sh - -c - nvidia-smi && sleep 3600 resources: limits: nvidia.com/gpu: 1

提交后可以查看调度结果:

kubectl apply -f gpu-test.yaml kubectl get pod gpu-test -o wide kubectl describe pod gpu-test | tail -20

如果 Pod 一直 Pending,并且事件里出现0/8 nodes are available,说明集群里没有感知到可用的 GPU。这时要依次检查节点驱动、容器运行时、Device Plugin 日志和资源上报。不要把问题直接归结为“节点没卡”,先确认调度器是否真的知道某台节点有卡。

在生产环境,更推荐的方案是使用 NVIDIA GPU Operator。它把驱动、设备插件、容器运行时、DCGM exporter 等组件打包,通过 Kubernetes Operator 统一部署和升级。这样做的好处是环境初始化变得可重复,避免每台节点手动装驱动的版本漂移。

2.2 GPU 的共享与分片:不能只按整卡租

早期 GPU 云只能按整卡出售,导致很多小任务浪费资源。现在演进出的方案主要有:

  • MIG:NVIDIA 官方支持将一个物理 GPU 切成多个独立实例,显存和计算单元隔离。
  • 软件分片:基于 CUDA MPS、显存限制、时间片调度实现算力共享。
  • 虚拟 GPU:企业级 vGPU 方案,适合桌面虚拟化或部分推理场景。
方式隔离程度适用场景常见问题
整卡多卡训练、推理服务小任务浪费资源
MIG较强中小推理、开发调试需要驱动和容器支持
软件分片较弱批量推理、CI 测试稳定性依赖限制策略
时间片短任务、交互式实验长任务性能波动明显

开发者在提交任务前,应该确认所用实例是否启用了分片。比如使用 MIG 时,nvidia-smi的输出里会有 MIG 设备列表;使用时间片共享时,不同进程会占用同一个物理卡,显存和算力都可能有竞争。如果直接把共享实例当成整卡性能使用,训练耗时会出现明显波动。

适合用共享实例的场景包括:推理服务中的小型模型、批量数据处理、多组并行实验、CI 中的回归测试。不适合用共享实例的场景包括:需要稳定算力的大模型训练、对延迟敏感的在线推理。

2.3 InfiniBand、RDMA 与存储:多卡训练的关键

单机多卡训练需要高速卡间通信,NVLink 解决单机内互联;跨机训练常见方案是 InfiniBand 或 RoCE 网络。以 NCCL 为例,多节点通信时大量小消息需要低延迟和足够带宽。如果网络只有普通千兆或 25G Ethernet,分布式训练可能会在 allreduce 阶段严重拖慢。

验证网络是否正常,可以用nccl-tests

mpirun --hostfile hosts -np 8 \ -x LD_LIBRARY_PATH=/usr/local/nccl/lib \ ./build/all_reduce_perf -b 8M -e 8G -f 2 -g 1

对比不同网络下的 allreduce 吞吐量,可以判断集群是否适合多卡训练。很多算力平台“单卡很快、多卡反而不如单卡”,原因就出在网络拓扑和 NCCL 通信路径配置上。比如没有配置NCCL_IB_DISABLE=0,或NCCL_SOCKET_IFNAME指向了错误网卡,都可能导致多卡通信退化到 TCP。

存储同样不能忽略。训练数据通常要经过对象存储、分布式文件系统或本地 SSD 缓存。如果数据加载时间比 GPU 计算时间还长,利用率不可能高。观察 GPU 空转和数据读取速度,可以用nvidia-smi看 GPU 利用率是否在训练过程中长期处于低位,再用iostatdstat看磁盘和网络 IO。数据准备阶段最好提前把数据集缓存到本地或高速分布式存储,避免每个训练 step 都从远程拉取。

2.4 功耗与散热:稳定运行的隐藏成本

高密度 GPU 服务器的功耗远高于普通 CPU 服务器。一台八卡训练服务器满载功耗可能达到数千瓦。传统风冷在散热效率、噪音和机房承重上都有瓶颈,所以大规模 GPU 云普遍转向液冷。

对使用者来说,功耗和散热影响的是实例的持续负载上限。遇到“跑一段时间后降频”的问题,优先检查 GPU 温度和功耗限制:

nvidia-smi -q -d TEMPERATURE nvidia-smi -q -d POWER nvidia-smi --query-gpu=name,temperature.gpu,power.draw,power.limit --format=csv

如果温度接近 85 度或者功耗被限制在较低数值,说明节点存在散热瓶颈。这种环境下,持续训练任务的性能会明显低于基准测试。云用户通常看不到机房硬件细节,但通过监控温度曲线和性能波动,可以判断是否遇到了降频。

注意:不要只验证程序能启动,还要验证持续负载下的温度、功耗、网络和存储表现。闪存级的峰值性能不代表长时间训练稳定。

3. 开发者租用 GPU 云的环境准备与最小任务

3.1 先判断场景:训练、推理、渲染

不同场景对 GPU 的要求不同。大模型训练更依赖显存、多卡通信和存储带宽;在线推理更看重延迟、吞吐和并发;渲染和视频处理则偏重图形计算和编解码能力。选型之前先明确场景,否则即使节点提供了很高算力,也不一定能满足业务要求。

一个简单的判断方法:

  • 训练任务:关注显存容量、卡间拓扑、网络带宽、数据加载。
  • 推理任务:关注延迟、服务化能力、动态批处理支持。
  • 调优和实验:可以选性能低一些、价格更便宜的规格。
  • 数据预处理:可能完全不需要 GPU,直接用 CPU 节点更划算。

很多团队把不需要 GPU 的数据清洗任务也提交到 GPU 队列,白白占用资源。解决方式是给不同队列打标签,明确哪些任务允许申请 GPU,哪些任务只能使用 CPU。

3.2 查看可用 GPU 型号和实例规格

在 GPU 云中查看可用资源的方式各不相同。通过 Kubernetes 节点标签,可以列出 GPU 类型:

kubectl get nodes -L gpu-type,nvidia.com/gpu.product kubectl describe node <node-name> | grep nvidia.com/gpu

如果输出中没有nvidia.com/gpu,说明节点没有正确注册 GPU 资源。此时需要确认节点是否安装了 NVIDIA 驱动、容器运行时是否启用了 NVIDIA 支持,以及 Device Plugin 是否运行正常。

使用云控制台时,也要注意实例规格的“GPU 型号”和“显存大小”可能不是同一维度。有些实例名里带A100-40G,有些只写A100,实际可能是 80G 或 40G 版本。训练大模型前一定要看显存上限。

3.3 提交一个最小 GPU 任务

下面用一个最小 PyTorch 任务验证环境。先准备 Dockerfile:

FROM pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime WORKDIR /workspace COPY train.py . CMD ["python", "train.py"]

train.py打印是否支持 CUDA:

import torch print("cuda available:", torch.cuda.is_available()) print("device count:", torch.cuda.device_count()) print("device name:", torch.cuda.get_device_name(0)) a = torch.randn(1000, 1000, device="cuda") b = torch.randn(1000, 1000, device="cuda") print((a + b).sum().item())

在 Kubernetes 中提交:

apiVersion: batch/v1 kind: Job metadata: name: torch-gpu-check spec: template: spec: restartPolicy: Never containers: - name: torch image: your-registry/torch-check:v1 resources: limits: nvidia.com/gpu: 1

查看日志:

kubectl logs job/torch-gpu-check

预期输出中cuda available: True,并且计算得到一个浮点数。如果cuda available: False,大概率是 PyTorch 版本与驱动不匹配,或容器运行时没有启用 GPU。

3.4 验证命令与环境检查清单

在容器中执行:

nvidia-smi nvidia-smi --query-gpu=index,name,memory.total,memory.used,utilization.gpu,temperature.gpu --format=csv

正常情况下会显示一张或多个 GPU。如果出现No devices were foundFailed to initialize NVML,要先检查容器运行时和驱动版本。

这里整理一份租用 GPU 前的环境检查清单:

  • GPU 型号是否与业务匹配,显存是否够用。
  • 驱动版本是否覆盖你需要的 CUDA 版本。
  • 容器运行时是否支持 NVIDIA GPU。
  • Device Plugin 是否正常上报资源。
  • 数据存储位置是否与计算节点在同一内网。
  • 多卡任务是否确认了机器间网络类型。
  • 是否有监控可以查看 GPU 利用率和温度。
  • 是否设置了最大预算和任务超时时间。

这张清单在每次新环境都可以复用。很多“代码在本地跑没问题,到云端就失败”的案例,本质是上面某一项没有对齐。

4. 成本、弹性与容错:把 GPU 用到刀刃上

4.1 三种租用模式怎么选

模式特点成本风险合适场景
按需实例即开即用,无需等待无抢占生产、即时任务
预留容量提前锁定资源利用率不足时浪费长期稳定负载
竞价/抢占实例价格低,可能被回收任务中断容错训练、批处理

按需实例适合需要快速上线的生产任务,但价格最高。预留容量适合 7×24 的训练任务,如果任务经常中断或使用不足,预留反而浪费。竞价实例价格低,但一定要设计好容错逻辑,否则一次抢占可能导致整轮训练重新开始。

4.2 成本公式:真正影响账单的四个因素

GPU 云账单并不是“GPU 单价 × 运行时间”这么简单。实际成本至少包括:

  • 实例运行时长,是否一直挂机但没有训练。
  • 存储和网络流量,尤其是模型文件和数据集的上传下载。
  • 快照和镜像备份,低频备份也会占用费用。
  • 资源利用率,申请了 8 卡但只跑满 2 卡,浪费的是无法退还的成本。

降低成本的第一个动作是给任务设置超时。长时间不释放的开发环境实例,会不断产生费用。第二个动作是压缩镜像和数据集:减少不必要的依赖、使用轻量基础镜像、把不常变的数据放到共享存储。第三个动作是监控利用率:如果 GPU 长期低于 30%,就该考虑拆分任务或换小规格。

4.3 用断点续训提升长时间任务稳定性

长时间训练最怕中断。推荐做法是:

  • 每 N 步保存模型权重和优化器状态。
  • 保存文件名称带 step,失败后从最近 checkpoint 恢复。
  • 将 checkpoint 放到共享存储或对象存储,避免节点故障后数据丢失。

示例保存逻辑:

checkpoint = { "model": model.state_dict(), "optimizer": optimizer.state_dict(), "step": step, "lr": scheduler.get_last_lr(), } torch.save(checkpoint, f"checkpoint-{step}.pt")

恢复时先加载 checkpoint 再继续训练:

ckpt = torch.load(f"checkpoint-{latest_step}.pt") model.load_state_dict(ckpt["model"]) optimizer.load_state_dict(ckpt["optimizer"]) step = ckpt["step"]

不要只保存权重,不保存优化器和学习率调度器状态。否则恢复后学习率会重新开始,训练曲线可能产生明显波动。

4.4 弹性伸缩不能只看 CPU

GPU 资源昂贵,伸缩策略要更谨慎。如果推理服务的负载波动大,可以基于请求队列长度或 GPU 利用率扩缩容。但要避免频繁进出导致的冷启动和成本抖动。

在 Kubernetes 中,Horizontal Pod Autoscaler 支持自定义指标。可以从 Prometheus 查询 GPU 利用率作为伸缩依据。但生产环境要设置最小副本数和冷却时间,防止流量抖动造成反复扩缩。对于训练任务,也不要盲目追求“越大越好”,显存不足时会 OOM,显存过多时则浪费配额。先做一个小 batch 的显存测试,再决定是否扩大 batch size 或切换多卡。

5. 从“能运行”到“生产可用”的排查链路

5.1 现象一:Pod 一直 Pending

可能原因:

  • 节点 GPU 资源不足。
  • Device Plugin 未上报资源。
  • 节点标签或亲和性不匹配。

检查方式:

kubectl describe pod <pod-name> | grep -A10 Events kubectl get nodes -o wide

处理方式:先调整请求的 GPU 数量,再确认节点驱动和插件状态。不要直接扩容节点,先看是不是资源分配粒度过大。比如一个推理服务只需要 1GB 显存,却申请了整张 80GB 的卡,导致同一节点多开几个 Pod 就满了。

5.2 现象二:CUDA 初始化失败

错误示例:

CUDA error: no kernel image is available for execution on the device RuntimeError: Found no NVIDIA driver on your system

可能原因:镜像中的 CUDA 版本与节点驱动不匹配,或容器运行时没有把宿主机 GPU 传入容器。

检查方式:

nvidia-smi | grep "Driver Version" python -c "import torch; print(torch.version.cuda)"

处理方式:根据节点驱动调整镜像中的 CUDA 基础版本,或更新驱动。生产环境最好由平台统一维护基础镜像,减少使用者的匹配成本。项目里可以约定基础镜像仓库,不同团队共用,避免每个开发者各自维护一套 CUDA 环境。

5.3 现象三:显存 OOM 或任务被杀

可能原因:请求的显存超过物理显存,或共享实例上其他任务占用了资源。

检查方式:

dmesg | tail -20 nvidia-smi --query-gpu=memory.used,memory.total --format=csv

处理方式:将批处理大小调小,开启梯度累积;如果是共享实例,需要确认资源隔离是否真正生效。注意,有些容器能看到全部显存,但实际只允许使用一部分;如果设置了NVIDIA_VISIBLE_DEVICES没有正确传入资源限制,任务可能看到超过配额的总显存,然后在分配时失败。

5.4 现象四:多卡训练越来越慢

可能原因:网络带宽不足、NCCL 通信配置错误、数据加载成为瓶颈。

检查方式:使用nccl-tests查看 allreduce 性能,并在训练日志里观察每个 step 时间。如果 step 时间随卡数增加不降反升,大概率是通信瓶颈。

处理方式:确认多节点任务是否使用了 RDMA 网络;训练脚本里设置正确的NCCL_SOCKET_IFNAME等环境变量;将数据缓存到本地 SSD。另外要注意,多卡训练时 PyTorchDataLoadernum_workers不够会让数据加载变成瓶颈,GPU 一直等数据,多卡作用会被削弱。

现象常见原因检查命令处理建议
Pending资源不足或插件异常kubectl describe pod调整申请量或修复插件
CUDA 失败驱动与镜像不匹配nvidia-smitorch.version.cuda统一基础镜像版本
显存 OOMbatch size 过大nvidia-smi调小 batch 或使用梯度累积
多卡变慢网络、存储或 DataLoader 瓶颈nccl-tests、step 耗时检查 RDMA、缓存和 worker 数量

6. 学习环境、生产环境和自建机房怎么取舍

6.1 学习环境怎么快速验证

学习阶段不必追求多卡,先用一张单卡把训练代码跑通。选择规格时优先看显存是否满足模型需求,再看价格。环境尽量和最终生产保持一致,避免“本地能跑、线上不行”。

建议的学习路径是:

  1. 先租一个最便宜的 GPU 实例,跑通nvidia-smi和 PyTorch 最小计算。
  2. 用小模型训练一轮,观察显存和 GPU 利用率。
  3. 再把训练脚本改成支持 checkpoint。
  4. 最后才考虑多卡分布式训练和弹性伸缩。

不要一开始就搭复杂的 Kubernetes 集群。学习阶段先把“容器里用 GPU”的原理搞清楚,再上升到集群调度,排查问题会更容易。

6.2 生产环境需要补什么

生产环境至少要考虑:

  • 算力配额和权限分离,避免误删任务或超支。
  • 监控告警:GPU 利用率、温度、显存、功耗、网络指标。
  • checkpoint 自动备份和故障自愈。
  • 镜像仓库和版本管理,保证复现性。
  • 预算控制:设置月额度、告警阈值,防止竞价实例被刷爆。

权限分离很容易被忽略。如果每个算法工程师都能直接删除命名空间,出现误操作时无法追溯。合理的做法是按项目分命名空间,再通过 RBAC 分配不同角色;使用云控制台时,则按云账号的子账号分配权限。

注意:生产环境的 GPU 任务要有明确的重试策略和可观测性,不能只依赖人工盯日志。

6.3 选型清单:GPU 云还是自建

考量点GPU 云自建机房
初始成本
扩容速度
运维负担
长期成本取决于利用率利用率稳定时更有优势
适合场景波动需求、快速实验长期稳定、数据敏感

决策建议:如果数据敏感度不高、任务波动大,优先使用 GPU 云;如果长期 7×24 满载运行,并且对数据主权有要求,再评估自建。不要只按单价比较,要把排队、故障恢复、网络带宽和运维人力都折算进总成本。

对 CoreWeave 这类 GPU 云服务商来说,转机的真正来源不是单一订单,而是通过调度、网络、存储和散热等技术手段,让每一块 GPU 都更接近满载状态。对使用算力的开发者来说,同样要练好基本功:先确认场景,选对实例,跑通最小任务,再逐步压测和优化。能从日志和指标里定位问题的人,在任何 GPU 平台上都能获得更稳定的结果。

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

Zen Browser扩展完整指南:3步让你的浏览体验各归其位

Zen Browser扩展完整指南&#xff1a;3步让你的浏览体验各归其位 【免费下载链接】desktop Welcome to a calmer internet 项目地址: https://gitcode.com/GitHub_Trending/desktop70/desktop 你有没有过这种时刻&#xff1f;几十个标签页堆在侧边栏&#xff0c;想找的那…

作者头像 李华
网站建设 2026/8/31 12:39:51

游戏开发别写死参数!从0.1秒兜底到根因修复的正确姿势

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/8/31 12:39:20

基于Hermes Agent的五角色团队协作架构实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/8/31 12:38:48

爬虫先探测再抓取:ProbeScraper工具解析与实战

最近在 Hacker News 上看到一个很有意思的项目&#xff1a;Show HN: Scraper that probes a site before promising it works。一句话概括设计思路&#xff1a;在承诺“我能抓这个站”之前&#xff0c;先对目标站点做一次探测。这个思路看起来简单&#xff0c;但对做爬虫工程的…

作者头像 李华
网站建设 2026/8/31 12:34:11

YOLOv8果园成熟果实自动计数系统:从数据集到部署全流程解析

简介&#xff1a;本资源是一套基于YOLOv8的果园成熟果实自动计数完整实践项目&#xff0c;面向计算机科学、人工智能、自动化等专业的在校学生及初学者&#xff0c;解决农业场景中果实目标检测与精准计数的实际问题&#xff0c;适用于毕业设计、课程设计、大作业及项目立项演示…

作者头像 李华
网站建设 2026/8/31 12:33:11

纯惯导解算Matlab源码解析:从原理到工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华