news 2026/9/2 18:59:20

远程算力链路变局:AI工程师的GPU访问与切换预案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
远程算力链路变局:AI工程师的GPU访问与切换预案

美拟限制中国AI实验室远程访问海外芯片:算力链路的变局与工程师的技术预案

最近很多做模型训练的同学都在关注同一个话题:如果远程访问海外高性能GPU算力的通路发生变化,手头正在跑的预训练任务、微调任务、评测任务怎么办?

这不是一个纯粹的产业新闻,它其实戳中了所有AI工程师最依赖、也最容易被忽略的一层基础设施——远程算力链路。过去几年,很多团队习惯了通过SSH连到几千公里外的GPU集群上跑实验,用云厂商的A100/H800集群做训练,代码在本地,算力在远端。这套模式很成熟,但也很脆弱:只要链路中的任何一个环节发生变化——网络策略、云厂商服务条款、芯片供货、配额限制——整个训练流程就会进入不确定性。

本文不想评论政策本身,而是从技术视角拆穿三个问题。第一,远程访问海外AI芯片算力的技术链路到底是怎么搭起来的,哪里容易断。第二,如果这条链路被切断或降级,开发者在工程上有什么替代方案。第三,怎么从一开始就把训练环境设计成“算力可迁移、任务可切换”的形态,避免被锁死在某一类GPU、某一朵云、某一条网络通路上。

读完这篇文章,你至少能获得一份可操作的技术预案:怎么用容器固化训练环境,怎么用调度层把任务从A集群切到B集群,怎么用对象存储中转数据和模型,以及切换算力时常见的坑和排查思路。

1. AI训练为什么离不开“远程访问算力”

先说一个被很多人忽视的现实:大模型训练不再是一个“单机软件工程”问题,而是一个高性能计算基础设施调度问题。一个几十B参数的模型,预训练阶段动辄需要几百张乃至上千张高性能GPU组成集群,持续运行数周。这个规模的算力,绝大多数公司自建是不划算的,也很难在本地机房里实现短期弹性扩容。

所以,行业里自然形成了一种习惯:算力在哪,训练就往哪跑。早期是租物理机跑训练,后来是租云厂商GPU实例,再后来是直接使用云上的托管训练平台。工程师本地写代码,通过SSH、容器、任务调度器把代码和环境“送”到远端GPU机上,然后把数据和模型权重在本地与远端之间来回搬运。这就是远程访问算力最朴素的样子。

它看上去只是“多了一层网络操作”,但实际已经改变了AI工程的角色分工:

  • 本地负责开发、调试、可视化。
  • 远端负责大规模计算。
  • 云端的对象存储负责保存训练数据和检查点。
  • 任务调度系统负责排队、申请GPU、重启失败任务。

如果只用一句话总结:远程访问不再是一个临时的运维手段,而是AI训练链路中的核心通道。既然它是核心通道,就不能没有冗余,也不能不做故障预案。很多团队把所有鸡蛋放在一个“海外GPU集群”里,一旦访问通路出问题,整个项目就进入停摆状态,这才是最需要警惕的工程风险。

2. 远程访问海外AI芯片算力的技术路径拆解

要做好预案,先得看清现状。目前团队远程使用海外GPU算力,常见的路径有以下几类。

2.1 SSH直连GPU服务器

最经典的方式。团队在海外云厂商租下GPU裸金属实例或虚拟实例,管理员把公网IP、SSH端口和密钥发给开发人员,开发者在本地终端执行:

ssh -i ~/.ssh/id_ed25519 -p 22 ubuntu@your-gpu-host-ip

登录之后,一切操作和本地服务器几乎没有区别:安装依赖、拉代码、跑torchrun、用nvidia-smi看显存。

这个方式的好处是简单直接,自由度最高。坏处是难以协作:每个开发者的依赖环境可能互相干扰,多个人同时登录资源难管控,任务中断后也很难优雅恢复。

2.2 云厂商托管训练平台

如果不想自己维护环境,很多团队会使用云厂商提供的AI开发平台。例如 SageMaker、Azure Machine Learning、Google Vertex AI 这类服务,可以把训练代码、数据集、镜像提交上去,由平台自动申请GPU实例、执行训练并把模型输出写回对象存储。

这种方式把环境管理、资源生命周期、任务监控都抽象掉了,开发者只需要写训练脚本和定义估算规格。

2.3 容器镜像 + Kubernetes / Ray 集群

更工程化的团队会把远程算力池做成一个内部平台。代码统一打进Docker镜像,GPU机器加入Kubernetes集群或Ray集群,开发者提交一个训练任务,调度器负责分配GPU。这套方式既能支撑多人协作,也能做资源配额和故障恢复。

ray submit --address ray://your-cluster-head:10001 train_object_detection.py -- --epochs 50

2.4 数据与模型管理链路

无论哪种访问方式,数据和模型都躲不开“中转”。一般会依赖云厂商对象存储保存原始数据集、中间检查点和最终权重,训练机从对象存储拉数据,训练完成后把检查点传回去。

路径入口资源管理协作能力适用场景
SSH直连终端人工管理单人调试、小规模实验
云托管平台Web控制台/CLI平台托管标准训练流程
K8s/Ray集群任务提交命令调度器管理团队化、大规模训练

从风险角度看,SSH直连对链路依赖最重,云托管平台对厂商绑定最深,K8s/Ray相对灵活,但搭建成本高。选择哪条路径,直接影响后面切换算力的难易程度。

3. 远程算力链路的四类真实风险

很多人以为远程访问算力只是“网络连通”问题,实际拆开看,至少有四类风险会在关键时刻卡住项目。

3.1 网络链路与延迟

跨地域SSH、数据回传都依赖公网链路。远端GPU集群如果和本地代码仓库、数据集仓库不在同一个区域,每次拉取数据都要承受高延迟。训练任务一旦因为网络抖动导致数据加载超时,轻则单卡空转,重则任务失败。

更隐蔽的是,很多大公司内部出口会做流量审计、域名白名单、协议限制。如果出口策略临时收紧,恰好覆盖了你的SSH端口或对象存储域名,训练链路会直接中断。

3.2 数据安全与合规风险

训练数据中如果包含用户隐私、业务核心数据,把它们传到远端GPU上进行计算,就涉及数据被存储、被访问、被复制的问题。即使云厂商承诺加密,从风险角度讲,数据离开你控制的范围越远,安全边界就越模糊。

合规审计通常要求:数据流向清晰、访问日志可查、存储位置明确、传输过程加密。但很多开发者在跑远程训练时,并不会严格检查这些点,一旦需要追溯,会非常被动。

3.3 成本失控

GPU实例按小时计费,集群空闲等待也是钱。远程算力最大的隐性成本不是单卡时价,而是资源生命周期管理不严造成的浪费:训练完忘了释放实例、一个任务排队等了很久、多个人重复提交相同实验。等成本账单出来的时候,才发现一半的GPU资源都在空转。

3.4 供应链绑定

这是最容易被忽略的风险。训练代码深度依赖某类芯片的CUDA生态,优化脚本针对某厂商架构调整过,数据管道写死了某个海外对象存储路径。一旦需要从原来的算力池迁移到其他类型的芯片或区域,会发现每一层都是定制化的,迁移成本远超想象。

从工程角度讲,风险不是“会不会发生”,而是“发生时你能否快速迁移”。如果连“把训练环境换到另一批机器上”都做不到,那远程算力这条链路就成了一根没有备份的独木桥。

4. 工程师的应对思路:给算力链路做解耦

面对不确定性,正确的工程策略不是赌某个方向一定不会变,而是把训练系统和具体算力资源解耦。让训练脚本只关心模型和数据,让具体的芯片型号、云厂商、网络通路变得可替换。

4.1 环境可复现:用容器固化训练环境

不要依赖某台机器上手工安装的Python包。把环境写进Docker镜像或者Conda环境文件,确保任何一台新GPU机器都能在十分钟内恢复一致的训练环境。

# Dockerfile FROM nvidia/cuda:12.1.1-runtime-ubuntu20.04 ENV DEBIAN_FRONTEND=noninteractive ENV PYTHONUNBUFFERED=1 RUN apt-get update && apt-get install -y \ python3.10 \ python3-pip \ git \ curl \ && rm -rf /var/lib/apt/lists/* WORKDIR /workspace COPY requirements.txt . RUN pip3 install --no-cache-dir -r requirements.txt COPY train.py . COPY config.yaml . ENTRYPOINT ["python3", "train.py"]

镜像一旦构建好,无论是推到海外云厂商的镜像仓库,还是拉到本地集群,环境都是一致的。这一步解决的是“换了一批机器,依赖全部冲突”的问题。

4.2 任务入口与算力解耦:训练脚本只认参数

训练脚本不要硬编码数据路径和GPU数量,全部通过配置文件和命令行参数传入。这样同一份代码可以在不同GPU集群上运行,只需要改配置。

# config.yaml model: name: "bert-base-uncased" max_seq_len: 512 data: train_path: "s3://your-bucket/data/train.jsonl" val_path: "s3://your-bucket/data/val.jsonl" train: batch_size: 32 epochs: 3 learning_rate: 2e-5 use_fp16: true output: checkpoint_dir: "s3://your-bucket/checkpoints" model_save_path: "s3://your-bucket/models"
# train.py import argparse import yaml import torch def load_config(config_path: str): with open(config_path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def main(): parser = argparse.ArgumentParser() parser.add_argument("--config", type=str, default="config.yaml") parser.add_argument("--gpus", type=int, default=1) args = parser.parse_args() cfg = load_config(args.config) print(f"Load config: {cfg}") print(f"Training with {args.gpus} GPU(s)") if torch.cuda.is_available(): print(f"GPU device count: {torch.cuda.device_count()}") # 这里才是真正的训练逻辑,与具体集群无关 # ... if __name__ == "__main__": main()

这段代码本身不绑定任何云厂商。config.yaml里的路径指向对象存储,具体是S3、还是其他兼容S3的对象存储,只需要对应适配即可。训练脚本只关心“数据在哪”和“模型输出到哪”,不关心GPU机器背后的厂商和芯片架构。

4.3 数据和模型走对象存储,不要落在实例本地

训练机的本地磁盘通常是临时性的,实例释放后数据就没了。数据源和检查点应该持久化在对象存储中。训练开始时从对象存储拉取,训练过程中定期把checkpoint传回去,这样即使某台机器被回收,任务还能从最近的检查点恢复。

# 训练前同步配置和数据 aws s3 sync s3://your-bucket/data ./data # 训练过程中定期上传checkpoint aws s3 cp checkpoints/epoch_2.pt s3://your-bucket/checkpoints/ # 训练后同步模型 aws s3 sync ./models s3://your-bucket/models

用对象存储做中间层,带来的最大好处是:数据不再属于某台GPU机器,而是属于你的项目。机器可以换,集群可以换,数据始终在。

4.4 业务代码不写死任何云厂商API

很多团队在训练脚本里直接调用某个云厂商的专有SDK做数据读取,这是很危险的绑定。尽量使用通用协议,例如S3兼容的对象存储接口、HDFS、标准数据库连接串。如果必须使用厂商专属能力,把它封装在一个单独的模块里,不要散落到业务代码的各个角落。

5. 远程算力通路变化后的替代方案

如果原有的“远程访问海外GPU算力”这条链路真的变得不可用或不够稳定,技术上有哪些替代方向?下面按可行性和迁移成本来梳理。

5.1 自建本地集群 + 开源调度平台

如果是中等规模团队,可以在本地机房或国内数据中心搭建基于Kubernetes的GPU集群,配合NVIDIA Device Plugin或异构芯片的调度器,把GPU资源池化。开发者提交任务的方式和用远程集群几乎一致,牺牲的是弹性扩容的速度,换来的是对算力链路的掌控。

需要提醒的是,自建集群的运维成本很高,需要处理硬件故障、驱动升级、网络拓扑、任务调度、监控告警。不能只看到硬件成本,还要估算运维人力。

5.2 算力芯片选型从“单一起源”走向“多架构适配”

过去训练代码普遍只考虑CUDA生态,但迁移到其他架构芯片时,需要回到框架层去适配。现在的技术栈其实已经提供了不少通用层,例如:

  • PyTorch官方或社区提供的跨厂商后端。
  • ONNX Runtime作为推理中间层。
  • 面向统一编程模型的编译器方案,让同一份模型定义能在不同芯片后端上编译执行。
  • 各芯片厂商提供的自有训练框架。

迁移思路是:模型定义尽量保持标准PyTorch/TensorFlow格式,避免在算子实现里使用底层私有API。用torch.cuda.is_available()这类判断来控制硬件分支,而不是在模型结构里写死芯片类型。这样无论是切到其他型号GPU,还是切到其他架构芯片,都能降低迁移成本。

5.3 混合云与多云容灾

把训练架构设计成多集群模式。核心数据保存在自建或国内对象存储中,弹性训练任务可以按需漂移到多个算力池。一个簇出现不可用状态时,调度系统自动把任务提交到备用簇。这个思路和互联网应用的多云容灾一脉相承,只是“流量”换成了“训练任务”。

实现上不一定需要很复杂的平台,可以先从两层做起:

  • 第一层:所有训练镜像都推送到多个镜像仓库,保证任何一个仓库不可用时,其他仓库还能拉取。
  • 第二层:准备好可切换的训练入口脚本,通过环境变量决定把任务提交到哪个集群。

5.4 用模型优化降低算力依赖

如果算力密度下降,最直接的工程补偿是让模型更小、训练更快、推理更省算力。量化、剪枝、知识蒸馏、LoRA等微调方案,在保留模型效果的前提下显著降低显存占用。很多场景用几十B的稠密模型并配不上那么多算力,换成7B甚至更小的稀疏模型或蒸馏模型,配合量化推理,在性价比上反而更合理。

这一条建议不是让大家放弃大模型,而是提醒:算力少了,就更要用性价比最高的方式使用算力。

6. 最小实践:让训练任务可以在不同算力之间切换

下面用一个最小项目示例,演示“算力可切换”的训练任务应该长什么样。这个示例不依赖任何特定云厂商,你把它放在远程GPU集群、本地服务器或者是其他环境,都可以跑起来。

6.1 项目结构

llm-trainer/ ├── config.yaml ├── train.py ├── Dockerfile ├── requirements.txt ├── run.sh └── data/ └── sample.jsonl

6.2 依赖文件

# requirements.txt torch==2.1.0 transformers==4.36.0 datasets==2.16.0 pyyaml==6.0

6.3 训练入口脚本

# train.py import argparse import logging import yaml import torch from transformers import AutoTokenizer, AutoModelForCausalLM from datasets import load_dataset logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def load_config(config_path: str): with open(config_path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def main(): parser = argparse.ArgumentParser(description="Switchable remote training demo") parser.add_argument("--config", type=str, default="config.yaml") parser.add_argument("--gpus", type=int, default=1) args = parser.parse_args() cfg = load_config(args.config) logger.info("Config loaded: %s", cfg) # 硬件初始化:以通用 API 判断,而不是绑定具体芯片厂商 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") logger.info("Using device: %s", device) model_name = cfg["model"]["name"] tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name) # 数据加载:路径来自配置,训练脚本不关心数据实际存储在哪个环境 dataset = load_dataset("json", data_files=cfg["data"]["train_path"]) logger.info("Dataset size: %d", len(dataset["train"])) model.to(device) logger.info("Training start with %d GPU(s)", args.gpus) # 实际训练循环省略,核心是演示工程结构 if __name__ == "__main__": main()

6.4 可切换集群的启动脚本

# run.sh #!/usr/bin/env bash set -euo pipefail # 通过环境变量指定算力池,默认是 local CLUSTER_ENV="${CLUSTER_ENV:-local}" echo "[INFO] Current cluster: ${CLUSTER_ENV}" # 本地执行 if [ "${CLUSTER_ENV}" = "local" ]; then python3 train.py --config config.yaml --gpus 4 fi # 假设某个远程环境通过 ssh 访问,这里只是示例,非真实地址 # if [ "${CLUSTER_ENV}" = "remote-gpu-pool" ]; then # rsync -avz ./ ubuntu@remote-gpu-host:/workspace/train # ssh ubuntu@remote-gpu-host "cd /workspace/train && python3 train.py --config config.yaml --gpus 8" # fi # 假设某个集群环境通过 ray submit 提交 # if [ "${CLUSTER_ENV}" = "ray-cluster" ]; then # ray submit --address ray://ray-head:10001 train.py -- --config config.yaml --gpus 8 # fi

6.5 运行与验证

chmod +x run.sh ./run.sh

预期输出大致如下:

[INFO] Current cluster: local INFO __main__ : Config loaded: {'model': {'name': 'bert-base-uncased'}, ...} INFO __main__ : Using device: cuda INFO __main__ : Dataset size: 100 INFO __main__ : Training start with 1 GPU(s)

当你想切到另一个算力池时,只需要修改run.sh中对应的分支,并把CLUSTER_ENV设置为目标环境的名称。整个切换过程,训练脚本没有任何改动。

这个最小实践演示的核心思想是:把“在哪跑”和“跑什么”彻底分开。训练逻辑、依赖、数据路径都做到可配置,算力层的变化就不会侵入到核心代码里。

7. 常见问题与排查思路

远程算力链路切换和日常训练中,经常遇到的问题集中在连接、数据、性能、依赖四个方面。

问题现象可能原因排查方式解决方案
SSH连接超时或中断网络策略变动、公网IP变更、安全组未放行检查ssh -v输出,确认是否卡在连接阶段;尝试ping目标端口使用跳板机或内网通道;开启KeepAlive;改用支持断点重传的同步工具
训练数据拉取非常慢数据在远端对象存储,跨地域传输延迟高curl -w或云厂商CLI查看传输速率;检查是否走公网在算力池同一区域建立数据副本;用内网传输;大文件先压缩再传输
切换到新集群后模块找不到新环境缺少Python依赖或版本不一致执行python -c "import torch"验证依赖统一使用Docker镜像;固定requirements版本
GPU利用率很低数据加载成为瓶颈;模型单卡batch过小多次运行nvidia-smi观察利用率波动;检查DataLoader是否设置num_workers增加预取和并行加载;调整batch size;使用梯度累积
GPU实例释放后checkpoint丢失检查点只存在实例本地磁盘查看实例本地目录是否还有文件训练中定期同步checkpoint到对象存储;配置自动保存策略

排查顺序一般遵循“网络层 -> 数据层 -> 环境层 -> 代码层”。先确认机器的基本资源能看到,再确认数据可以访问,之后检查环境和依赖,最后才怀疑训练代码本身,不要一上来就改模型逻辑。

8. 最佳实践与工程建议

结合前面几节的内容,这里给出几条可以直接落地的工程建议,适合团队在做远程算力链路改造或日常训练时参考。

8.1 算力资源使用最小权限

远程GPU机器属于高价值、高危资源。不要把root密钥明文写在文档里或提交到Git仓库。建议用SSH密钥对+跳板机的方式管理,每个开发者使用独立密钥,通过用户组做权限隔离。训练环境里只安装必要的依赖,不运行权限过高的常驻进程。

8.2 日志必须中心化采集

远程训练最大的不便就是看不到实时日志。不要让日志只落在训练机的/tmp目录里,建议统一写到标准输出,由采集组件收集到中心日志平台。这样即使实例被回收,日志也不会丢。

8.3 训练前做小规模试运行

大规模任务提交前,先用1个GPU跑几十步,确认数据加载、前向计算、反向传播、checkpoint保存都正常,再提交完整任务。这能避免大批量投喂后才发现路径错误或数据格式问题。

8.4 固定依赖版本并定期更新

requirements.txt里不要写torch>=2.0这种宽泛版本,要固定到具体版本号。同时定期检查镜像里的安全漏洞,基础镜像如果长期不更新,可能会携带已知风险。

8.5 监控资源费用

为每个训练任务打上标签,通过云厂商的成本分组功能,按月统计不同项目、不同团队成员的GPU消耗。成本透明之后,资源浪费行为会大幅减少。

8.6 准备回滚方案

训练环境的变更要有回滚能力。新环境训练出现问题时,能够快速切回上一版本的镜像和上一版本的代码。建议用Git标签和镜像tag对应管理,训练前记录commit hash + image tag + config hash,做到可追溯、可复现。

9. 总结与后续学习方向

这一轮行业变化,给AI工程师最直接的提醒是:远程算力访问是一条工程链路,不是一台机器。我们依赖的SSH、云平台、对象存储、调度系统、芯片生态,每个环节都值得被当作“可能变化的组件”来设计,而不是默认它们永远可靠。

从实践角度看,这篇文章值得你带走三件事:

  1. 训练环境容器化,训练脚本参数化,数据和模型放到对象存储,这样算力切换时核心代码不需要改动。
  2. 替代方案不是单点选择,而是组合拳:自建集群、多架构适配、混合云、模型压缩可以同时推进。
  3. 切换算力以前,先做小规模验证,再确认日志、监控、成本核算都配齐,不要等到大规模训练中途再改。

如果你接下来想继续深入,建议关注这几个方向:

  • Kubernetes GPU调度与资源隔离,掌握如何在多卡分布式环境下做容错。
  • Ray分布式训练调度,理解任务队列、对象存储、自动扩缩容的组合方式。
  • 模型压缩技术,包括量化、蒸馏、剪枝,让有限的算力发挥更大的业务价值。
  • 多云架构下的数据同步与任务编排,把“切换算力”变成平台能力。

技术栈会变,芯片供应会变,网络通路也会变。但可以确定的是:一个把训练环境、数据链路和算力资源解耦的AI工程体系,适应变化的能力一定更强。

建议把这些方案整理成团队内部的算力应急预案,不一定要立刻大改架构,但要确保当变化来临时,团队知道第一步做什么、第二步做什么,而不是临时上线去“赌”一个新方案。

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

Windows下从源码编译Tesseract 5.0完整指南:避开CMake与DLL的坑

简介:这是一份OCR-Tesseract 5.0编译后的完整版本,专为需要快速集成OCR能力的开发者和技术爱好者准备。Tesseract 5.0引入深度学习模型,显著提升识别准确率,支持超过100种语言,并允许用户自定义训练,适用于…

作者头像 李华
网站建设 2026/9/2 18:57:28

​2026网站建设公司推荐:设计、信息架构和后续编辑能力缺一不可

摘要:网站建设公司推荐不是简单列出服务商名称,而是判断企业该选择标准化SaaS、海外工具还是定制交付。公开资料显示,企业线上展示、询盘和交易仍在持续增长;但项目是否划算,取决于业务复杂度、上线周期、技术维护和三…

作者头像 李华
网站建设 2026/9/2 18:57:11

MultiLCD库:一套代码驱动Arduino多种液晶屏

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

作者头像 李华
网站建设 2026/9/2 18:52:23

STM32开发入门:从环境搭建到LED点灯完整指南

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

作者头像 李华
网站建设 2026/9/2 18:49:53

基于本地大语言模型的离线智能脱敏系统PrivateRedact实战指南

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

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

技术PDF高效解析:从被动阅读到主动数据挖掘的工程实践

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

作者头像 李华