xLLM轻量级预训练架构:高效训练与显存优化实战

发布时间:2026/10/7 5:40:19

xLLM轻量级预训练架构:高效训练与显存优化实战 1. 为什么“轻量级”不等于“小模型”xLLM 的设计动机第一次看到“xLLM: 轻量级高效预训练架构”这个标题很多人会下意识地把它归类成“又一个把参数量砍小、跑在消费级显卡上的小模型方案”。这个理解只对了一半。xLLM 里的“轻量级”修饰的其实是预训练架构本身的计算效率而不是最终模型的绝对规模。换句话说它想解决的是在同样的硬件预算和同样的数据量下怎么让每一层、每一次前向与反向传播都少做无用功从而把训练吞吐拉上去、把显存占用压下来。这件事为什么值得单独拿出来讲因为过去几年大模型预训练的竞争很大程度上是“堆资源”的竞争——更大的参数量、更长的上下文、更多的 token。但真正在一线跑过预训练的人都知道账不是这么算的。一个 7B 的模型如果架构设计得笨重它的训练成本可能比一个结构更聪明的 13B 模型还高。显存墙、通信墙、算力利用率这三座大山往往在模型还没开始收敛之前就把预算烧光了。xLLM 的出发点就是在这三座山上同时做减法。从关键词“xLLM”和“预训练架构”来看这个方向的核心诉求可以拆成三层。第一层是架构层面的稀疏化与参数复用让模型不必为每个 token 激活全部参数第二层是训练层面的显存与通信优化比如激活重计算、梯度累积、张量并行的切分策略第三层是工程层面的算子融合与精度管理把 BF16、FP8 这些低精度格式用到位同时不牺牲数值稳定性。这三层不是孤立的任何一层的改动都会牵动另外两层所以 xLLM 更像是一套协同设计的方法论而不是某个单点技巧。我个人的判断是xLLM 这类架构真正的价值不在于它能训出一个多强的模型而在于它把“单位算力能换回多少有效学习”这个指标摆到了台面上。对于大多数团队来说你不可能拥有万卡集群你手里可能只有几十张卡甚至几张卡。在这种约束下一个高效的预训练架构带来的收益远比盲目追大参数要实在。接下来的内容我会围绕这套架构最可能采用的技术路线、实操中的关键取舍以及我踩过或见过的坑一层层拆开讲。2. xLLM 架构里最可能落地的几个核心机制2.1 稀疏激活与专家路由把算力花在刀刃上轻量级高效预训练架构最典型的特征就是不是所有参数都参与每个 token 的计算。这背后最成熟的思路是混合专家MoE的变体。传统稠密模型里一个 token 经过 FFN 层时要和全部参数做矩阵乘法而在稀疏激活架构里路由器Router会先判断这个 token 该交给哪几个专家处理只激活其中一小部分。假设总共有 16 个专家每次只激活 2 个那么理论上的计算量就降到了稠密版本的八分之一左右。但这里有个容易被忽略的细节路由本身是有成本的而且路由不均会导致负载倾斜。我见过不少团队兴冲冲上了 MoE结果发现训练速度不升反降原因就是少数几个专家被过度激活其他专家几乎闲置最后变成“伪稀疏”——计算量没省下来通信开销倒是翻倍了。xLLM 如果要做到真正的轻量高效路由策略必须解决两个问题一是负载均衡通常靠辅助损失auxiliary loss来约束每个专家的激活频率二是路由的确定性避免同一个 token 在不同 step 被分到完全不同的专家导致梯度震荡。实操中还有一个隐藏坑专家并行的通信模式。当专家分布在不同设备上时token 需要被发送到对应设备再取回这个 all-to-all 通信如果没和计算重叠好就会成为瓶颈。我的经验是专家数量不要一上来就设得太大先从 8 个专家、每次激活 2 个开始观察 GPU 利用率和通信占比再逐步往上加。xLLM 的“轻量”如果体现在这里那它大概率会在路由算法和通信调度上做文章而不是单纯堆专家数量。2.2 分组查询注意力与 KV 缓存压缩注意力机制是预训练里显存和计算的大头。xLLM 要在预训练阶段就做到高效注意力层的设计几乎不可能绕开。目前主流的高效注意力方案里分组查询注意力GQA是最平衡的一种它让多个查询头共享同一组键值头从而把 KV 缓存的大小成比例地降下来。相比多头注意力MHAGQA 在推理阶段的显存优势非常明显而在预训练阶段它也能减少注意力计算中的冗余。但预训练和推理对注意力的诉求不完全一样。预训练时序列长度往往很长注意力矩阵是 O(n²) 的这时候光靠 GQA 还不够通常还要配合滑动窗口注意力或者稀疏注意力模式。滑动窗口的思路是每个 token 只关注它前面固定窗口内的 token而不是全序列。这样做的好处是计算复杂度从平方级降到线性级代价是丢失了长距离依赖。xLLM 如果定位在“高效预训练”很可能采用“局部窗口 少量全局 token”的混合模式既保住效率又不至于让模型完全失去长程建模能力。这里有个参数需要特别留意窗口大小的选择。窗口太小模型学不到长距离关系下游任务表现会明显掉窗口太大效率优势又没了。我一般建议从 512 或 1024 起步根据验证集上的长文本任务表现来调。另外KV 缓存的压缩不只是 GQA 一条路还有量化 KV 缓存、跨层共享 KV 等做法这些在预训练阶段是否启用取决于你的显存预算和对最终精度损失的容忍度。2.3 激活重计算与梯度检查点用时间换空间的经典权衡显存不够是预训练中最常见的问题。xLLM 要“轻量”激活重计算activation recomputation几乎是标配。它的原理很简单前向传播时不保存中间激活值反向传播需要时再重新算一遍。这样显存占用能大幅下降代价是多了大约三分之一的前向计算量。听起来是亏的但在显存受限的场景下它让你能训更大的模型或更长的序列这笔账往往是划算的。不过激活重计算有个粒度问题。全量重计算是把整个 Transformer 层的激活都丢掉省显存最多但计算开销最大选择性重计算只丢掉注意力矩阵这类显存大户保留其他中间结果平衡得更好。我在实际项目里更倾向选择性重计算因为注意力部分的激活值通常是显存占用的主要来源重算它的性价比最高。xLLM 如果要在预训练阶段做到高效大概率会采用细粒度的重计算策略而不是一刀切。还有一个和它配套的技术是梯度累积。当单卡 batch size 受显存限制上不去时梯度累积可以让你在多个 micro-batch 上分别算梯度再累加等效于更大的 batch。但要注意梯度累积和 BatchNorm 这类依赖 batch 统计的层配合时会有问题不过现在大模型基本都用 LayerNorm 或 RMSNorm这个坑相对少了。真正需要小心的是学习率调度梯度累积改变了有效 batch size学习率和 warmup 步数都要相应调整否则收敛曲线会很难看。3. 预训练实操从数据管道到训练循环的关键取舍3.1 数据配比与课程学习别让脏数据拖垮高效架构架构再高效喂进去的数据如果质量差训练效率照样上不去。xLLM 这类轻量级架构的一个隐含前提是单位 token 的学习效率要高这就要求数据本身的信息密度足够。我在做预训练数据管道时最看重的不是数据量有多大而是去重做得够不够狠、质量过滤够不够严。重复数据会让模型反复学同样的东西浪费算力低质量数据则会引入噪声让模型在早期就学偏。具体操作上我一般会走这么几步先用 MinHash 或 SimHash 做近似去重把重复率压到 1% 以下然后用基于规则的过滤器去掉乱码、过短、语言混杂的样本最后用一个小型分类器给数据质量打分保留高分部分。这套流程听起来繁琐但它对训练效率的提升是立竿见影的。xLLM 如果强调高效那它的数据管道大概率也内置了类似的清洗逻辑而不是让用户自己从零搭。课程学习curriculum learning是另一个值得关注的策略。它的核心思想是先喂简单数据再逐步加入困难数据。对于轻量级架构来说这能帮助模型在早期快速建立基础能力避免一上来就被困难样本带偏。实现上可以按序列长度分阶段先训短序列再逐步拉长也可以按数据质量分阶段。不过课程学习的调度策略很敏感设计不好反而会拖慢收敛我的建议是先用固定配比跑一个 baseline确认架构本身没问题再尝试课程学习。3.2 并行策略组合张量并行、流水并行与数据并行的配比预训练绕不开并行。xLLM 要高效并行策略的选型就是核心工程问题。三种主流并行方式各有适用场景数据并行最简单每张卡持有完整模型副本处理不同数据但显存占用高张量并行把单层拆到多卡上适合单层参数巨大的情况但通信频繁流水并行把不同层分到不同卡上适合层数多的模型但会有流水线气泡。实际项目中这三者通常是组合使用的也就是所谓的 3D 并行。配比怎么定我的经验法则是先看单层能不能放进一张卡放不下就上张量并行再看总层数和卡数决定流水并行的切分点剩下的维度用数据并行填满。xLLM 作为轻量级架构单层参数量应该控制得比较克制所以张量并行的需求可能没那么强更多会依赖数据并行加流水并行。但这里有个坑流水并行的气泡率。如果 micro-batch 数量太少流水线启动和排空的开销占比会很高GPU 利用率上不去。一般建议 micro-batch 数量至少是流水线级数的 4 倍以上。通信优化也是重头戏。张量并行里的 all-reduce、流水并行里的点对点通信如果没和计算重叠就会成为瓶颈。我见过最有效的做法是把通信拆成更细的粒度让它在反向传播计算的同时进行。xLLM 如果在这方面有优化那它的“高效”就落到了实处而不只是架构图好看。3.3 学习率、批次与优化器的联动调参预训练的超参调优是个系统工程不能孤立地看学习率或 batch size。xLLM 这类架构因为计算效率高往往能支持更大的有效 batch size而大 batch 又要求更高的学习率来保持收敛速度。这里有个经验公式学习率大致和 batch size 的平方根成正比。比如 batch size 翻四倍学习率翻两倍左右。但这只是起点实际还要看模型深度、数据分布和优化器选择。优化器方面AdamW 仍然是预训练的主流但它有个显存问题每个参数要存一阶和二阶动量显存占用是参数量的两倍。对于轻量级架构如果显存紧张可以考虑Adafactor或Lion这类更省显存的优化器。Adafactor 通过分解二阶矩来降低显存代价是收敛可能稍慢Lion 则只用一阶动量显存占用和 SGD 相当但学习率需要设得更小。我在小规模实验里对比过Lion 在同等显存下能支持的 batch size 更大但最终精度有时会略逊于 AdamW需要根据任务权衡。还有一个容易被忽视的点是权重衰减。预训练阶段权重衰减通常设得比较小比如 0.1 或 0.01但具体值要和模型规模匹配。模型越大权重衰减可以适当调大防止过拟合。另外梯度裁剪的阈值也要留意太大起不到稳定作用太小会拖慢收敛一般设在 1.0 附近比较稳妥。4. 我在轻量级预训练架构上踩过的坑与排查思路4.1 损失尖刺不是数据问题而是数值精度在作祟训练到一半突然出现 loss spike然后模型再也回不到原来的收敛轨迹这是预训练里最让人头疼的问题之一。我一开始总以为是脏数据导致的花大量时间清洗数据结果发现换了一批数据还是会出现。后来定位下来根因往往是低精度训练下的数值溢出。BF16 的动态范围比 FP16 大但精度低某些层的激活值或梯度在累加时容易丢精度积累到一定程度就爆了。排查这个问题的完整链路是这样的先看 loss spike 出现的 step 附近有没有异常大的梯度范数如果有就定位到具体是哪一层然后检查那一层的输入激活值范围看是否超出了当前精度格式的安全区间最后确认是注意力 softmax、LayerNorm 还是残差连接处出的问题。我的修复方案通常是在敏感层保留 FP32 计算比如 LayerNorm 和 softmax 用 FP32其余用 BF16。这个混合精度策略虽然增加了一点显存和计算但换来了训练稳定性非常值得。还有一个隐蔽的坑是优化器状态的精度。AdamW 的一阶和二阶动量如果也用 BF16 存长期累积误差会很大。稳妥的做法是优化器状态始终用 FP32哪怕模型参数是 BF16。这一点在显存规划时就要算进去别等到训练崩了才想起来。4.2 吞吐上不去先查数据加载再查通信重叠架构设计得再高效如果数据管道供不上GPU 照样空转。我遇到过好几次训练吞吐远低于预期的情况最后发现瓶颈根本不在模型而在数据加载。DataLoader 的 worker 数量不够、预处理太慢、或者磁盘 IO 跟不上都会让 GPU 等数据。排查方法很简单用 nvidia-smi 或 nsight 看 GPU 利用率如果它呈锯齿状波动一会儿 100% 一会儿 0%那基本就是数据供给的问题。解决思路分两层。第一层是增加 DataLoader 的并行度把 worker 数量调到 CPU 核心数的七八成同时用预取prefetch让数据加载和计算重叠。第二层是优化数据格式把原始文本提前 tokenize 好存成二进制训练时直接读省掉在线 tokenize 的开销。这一步对吞吐的提升非常明显我做过对比预处理后的数据管道能让 GPU 利用率从 60% 拉到 90% 以上。如果数据没问题那就查通信。张量并行的 all-reduce 如果没和反向传播重叠就会在每层结束时卡一下。这时候可以尝试把通信切分得更细或者调整并行策略减少通信密集的切分方式。xLLM 如果宣称高效那它在通信调度上应该有专门的优化但作为使用者你仍然需要根据自己的硬件拓扑去调并行配置不能指望开箱即用就达到最优。4.3 显存碎片为什么释放了缓存还是 OOM显存 OOM 是预训练的家常便饭但有一种 OOM 特别气人明明用 torch.cuda.empty_cache() 释放了缓存显存占用也显示降下来了可一跑训练还是爆。这通常是显存碎片导致的。PyTorch 的缓存分配器会把显存切成块来管理频繁申请和释放不同大小的张量后缓存里会留下很多不连续的小空洞虽然总空闲显存够但没有一块连续空间能放下新的大张量。应对碎片问题我一般会做两件事。一是固定序列长度避免动态长度导致张量大小频繁变化这样分配器更容易复用内存块。二是设置环境变量 PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True让分配器支持可扩展段减少碎片。这个设置在新版 PyTorch 里效果很好能明显降低碎片导致的 OOM 概率。另外如果用了激活重计算注意重计算时的峰值显存可能比预期高规划显存时要留出余量。还有一个和显存相关的经验不要盲目追求大 batch。有时候把 batch size 调小一点配合梯度累积反而能让训练更稳、显存更宽裕。xLLM 的轻量级特性给了你调大 batch 的空间但空间不等于必须用满找到吞吐和稳定性的平衡点才是关键。5. 把 xLLM 用起来一套可复现的最小训练配置5.1 环境准备与依赖版本锁定要复现一个轻量级高效预训练架构环境一致性比什么都重要。我见过太多“在我机器上能跑”的案例最后都是 CUDA、PyTorch、cuDNN 版本不匹配导致的。我的建议是用容器把环境锁死基础镜像选 PyTorch 官方镜像CUDA 版本根据你的显卡驱动来定。如果是 A100 或 H100CUDA 12.x 配 PyTorch 2.1 以上比较稳如果是消费级卡CUDA 11.8 的兼容性更好。依赖方面除了 PyTorch通常还需要 transformers 做 tokenizer、datasets 做数据加载、flash-attn 做高效注意力。flash-attn 的安装是个坑它需要和 PyTorch、CUDA 版本严格对应编译时间还长。我的做法是直接用官方预编译的 wheel别自己从源码编除非你有特殊需求。另外deepspeed 或 megatron 这类并行框架如果要用也要提前确认版本兼容性它们的 API 变动比较频繁。提示环境搭好后先跑一个单卡的小规模训练确认前向、反向、优化器更新都能正常跑通再上多卡并行。很多并行相关的 bug 在单卡上是看不出来的但单卡能排除掉大部分数据和张量形状的问题。5.2 一个可跑通的训练循环骨架下面这段代码是一个简化的训练循环骨架重点展示梯度累积、混合精度和激活重计算的配合方式。实际使用时并行部分需要替换成 DeepSpeed 或 Megatron 的对应实现。import torch from torch.cuda.amp import autocast, GradScaler # 假设 model、dataloader、optimizer 已经定义好 model.train() scaler GradScaler() accum_steps 4 # 梯度累积步数 optimizer.zero_grad() for step, batch in enumerate(dataloader): with autocast(dtypetorch.bfloat16): # 前向传播激活重计算通过 model 内部的 checkpoint 机制触发 outputs model(**batch) loss outputs.loss / accum_steps # 反向传播scaler 在 BF16 下其实可以不用但保留兼容 FP16 scaler.scale(loss).backward() if (step 1) % accum_steps 0: # 梯度裁剪防止梯度爆炸 scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) scaler.step(optimizer) scaler.update() optimizer.zero_grad() # 日志记录观察 loss 和梯度范数 if step % 100 0: print(fstep {step}, loss {loss.item() * accum_steps:.4f})这段代码里有几个细节值得说。第一loss 除以 accum_steps是为了让累积后的梯度等效于大 batch 的平均梯度别忘了这一步否则学习率会偏大。第二梯度裁剪在 scaler.unscale_ 之后做因为 scaler 会缩放梯度不还原的话裁剪阈值就不准了。第三BF16 下其实不太需要 GradScaler但如果你混用 FP16它就必须保留。第四激活重计算是通过模型内部的 checkpoint 包装实现的通常用 torch.utils.checkpoint.checkpoint 包住 Transformer 层即可不需要在训练循环里手动处理。5.3 监控指标别只看 loss训练过程中loss 只是最表层的指标。要判断一个轻量级架构是否真的高效我通常会盯这几个数GPU 利用率反映计算是否饱和、显存占用峰值反映空间效率、每秒处理的 token 数反映吞吐、梯度范数反映训练稳定性。这几个指标要一起看单看任何一个都可能误判。比如 GPU 利用率高但 token 吞吐低说明计算量大但有效学习少可能是架构里有冗余计算显存占用低但吞吐也低说明可能卡在通信或数据加载上。我习惯用一个简单的表格来记录每个 step 的这些指标训练结束后画成曲线一眼就能看出瓶颈在哪。指标正常范围异常时的可能原因GPU 利用率85% 以上低于 70% 查数据加载或通信显存峰值不超过卡容量的 90%接近 100% 考虑重计算或减小 batchtoken 吞吐随 batch 线性增长不增长查并行效率梯度范数0.1 到 10 之间持续大于 10 查学习率或数据这张表是我自己总结的不一定适用于所有场景但作为一个快速排查的参考很实用。尤其是梯度范数它是最早能反映训练异常的指标比 loss 还灵敏。6. 轻量级架构的边界什么时候该换回稠密模型xLLM 这类轻量级高效架构不是万能的。它的优势场景很明确算力受限、显存受限、但对训练吞吐有要求。如果你的团队有充足的 A100 集群数据量也足够大那稠密模型在最终精度上往往还是更稳因为稀疏架构的路由和通信开销在大规模下会被放大。我个人的经验是当你的卡数超过 64 张、且追求极致精度时稠密模型加成熟的 3D 并行仍然是更省心的选择。另一个边界是任务类型。轻量级架构在通用语言建模上表现不错但在需要精细推理或长链条逻辑的任务上稀疏激活可能让模型学不到足够密集的表示。这时候要么增加激活的专家数量要么干脆换回稠密架构。判断标准很简单在验证集上跑一组下游任务如果稀疏版本的差距在 2% 以内那效率优势就值得如果差距超过 5%就得重新考虑架构选型了。最后分享一个我在实际项目里的体会架构的效率提升是有上限的数据质量和训练策略的收益往往更大。我见过太多团队在架构上反复折腾却忽略了数据去重和课程学习这些“笨功夫”。xLLM 给了你一个高效的骨架但真正决定模型好坏的还是你喂给它什么、怎么喂。把数据管道做扎实把超参调稳比换一个更花哨的架构要实在得多。
延伸阅读

更多相关文章

2026/10/7 5:35:19

从n阱到三阱:CMOS阱结构如何决定芯片的衬底隔离能力

做混合信号芯片的人,迟早会被一个问题卡住:你明明把模拟模块和数字模块分开放了,电源也各自接了,可数字模块一翻转,模拟前端的输出还是一抖一抖的。查来查去,十有八九是衬底耦合噪声。而衬底噪声这笔账&…

2026/10/7 5:35:19

Jev小脑引擎:毫秒级动态决策快照与Agent决策分流架构

1. 项目概述:不是给Agent加个“插件”,而是重装一套神经反射系统“告别大模型废话!给 Agent 装上 Jev‘小脑’:70ms 决策实战与成本暴降 90% 的秘密”——这个标题里藏着三个被绝大多数人忽略的真相:第一,“…

2026/10/7 5:35:19

复古侦探书房场景设计:从物件选择到光线氛围的完整指南

1. 场景定位与整体设计思路1.1 这个场景到底在做什么“内景 复古侦探书房室内场景”这个标题,第一次看到的时候我脑子里蹦出来的画面很具体:一间光线偏暗的屋子,木质书架上塞满了皮面旧书,桌面上摊着卷宗和放大镜,角落…

2026/10/7 7:50:25

运动营养代工怎么选?ODM 研发能力决定产品品质

国内运动营养市场持续扩容,大量品牌方选择代工模式推出蛋白粉、肌酸等运动补剂。很多品牌方与普通消费者在挑选代工工厂时,只关注报价和交付速度,忽略工厂的底层研发实力。市面上不少小型代工企业仅能做简单贴牌加工,没有独立配方…

2026/10/7 7:50:25

一条龙服务!ClaudeCode新功能goal详解与TaoToken统一Key接入实践

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

2026/10/7 7:50:25

MCP协议实战:让大模型自己调用工具,从配置到验证一次跑通

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

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/6 4:01:51

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/6 17:46:51

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

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

2026/10/7 1:05:03

ESP32免重刷固件:浏览器直接修改NVS键值实现WiFi配置更新

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

2026/10/7 1:05:03

SAP HANA查询结果导出CSV:避开乱码、性能与权限的实用指南

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

2026/10/7 1:05:03

数字后端Placement阶段Density与Congestion控制实战

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

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑