【从零开始学深度学习编译器】十九,MLIR的Pass机制实践

0x0. 前言

这个系列的前面几篇文章对mlir的组件有了一些粗浅的认识,这篇文章不继续讲mlir的架构。而是从实践的角度带读者来看一下,mlir帮助我做了什么,这里仍然以oneflow dialect为例。在mlir:摩尔定律终结的编译器基础结构 论文解读 这篇文章的评论部分已经简单介绍了oneflow dialect相关的组件是如何实现的。在实现了oneflow dialect的基础上,我继续来介绍一下mlir的pass机制是如何助力oneflow模型训练和推理加速的。

0x1. 背景

当前Transformer架构已经成为做AI的算法开发人员和工程师们不得不谈的基础架构。由Transformer基础架构派生出了一系列超大模型如Bert和GPT-2,在业界都有非常大的影响,并且也引领了大模型的潮流。然而大模型的高昂训练成本让很多人甚至很多公司望而却步,通常只能在预训练的大模型上做一些下游任务,因此如何加速大模型训练是十分重要的。在2019年,英伟达成功地构建并训练了最大的语言模型 GPT-2 8B,这一模型包含 83 亿参数量,是 BERT-Large 模型的 24 倍、GPT-2 的 5.6 倍。英伟达将这一模型称为「Megatron」(威震天),还开源了用来训练这一模型的 pytorch 代码:https://github.com/NVIDIA/Megatron-LM。

这篇论文中提到了很多加速大模型训练的手段,特别的如模型并行训练技术,但本人对分布式训练了解很少这里不做介绍。我这里唯一的关注点是在Megatron论文(https://arxiv.org/pdf/2104.04473.pdf)的4.2节中提到的编译优化加速模型训练:

【从零开始学深度学习编译器】十九,MLIR的Pass机制实践

Megatron 4.2节

立即进入“豆包AI人工智官网入口”;

立即学习“豆包AI人工智能在线问答入口”;

【从零开始学深度学习编译器】十九,MLIR的Pass机制实践

Megatron 4.2节

立即进入“豆包AI人工智官网入口”;

立即学习“豆包AI人工智能在线问答入口”;

这一节编译优化讲的主要是可以通过PyTorch JIT技术来做一些Op融合,比如将bias_add和gelu融合成一个算子,bias_add+dropout融合成一个算子。做了这些算子融合之后,不仅可以避免GPU重复读写数据减少显存占用,还可以减少cuda kernel launch次数对整个计算过程进行加速。

要实现论文中提到的编译优化,需要两个前置条件。一是框架提供了融合Op的实现,二是基于编译器实现一个优化Pass来自动寻找模型中可以融合的Pattern并将其重写为等价的融合Op,达到对计算图进行运行加速的目的。

0x2. BiasAdd Dropout以及融合算子简介

在OneFlow中为了对标Megatron的bias_add和dropout fuse,实现了一个fused_bias_add_mask_scale算子,做的事情就是将BiasAdd和Dropout融合成一个算子来加速。这个算子的实现过程这里不展开,重点是如何在模型中基于MLIR自动发现这种Pattern并自动将这种Pattern替换为fused_bias_add_mask_scale算子。

为了下一节更好的理解融合Pass的做法,这里对bias_add,dropout以及fused_bias_add_mask_scale这三种Op的参数列表进行简要介绍。

bias_add 算子:代码语言:javascript代码运行次数:0运行复制

>>> import oneflow as flow>>> x = flow.randn(2, 3)>>> y = flow.randn(3)>>> z = flow._C.bias_add(x, y, axis=1)

可以看到这个算子有3个参数,一个是输入Tensor,一个是bias Tensor,还有一个axis属性表示需要把bias Tensor附加到输入Tensor的哪个维度上。在Transformer结构中,带偏置的线性层(nn.Linear)就是通过一个矩阵乘法(matmul)算子和一个bias_add实现的。

nn.Dropout 算子:Dropout算子相信大家非常熟悉,不需要多解释,可以参考下方OneFlow算子文档。

【从零开始学深度学习编译器】十九,MLIR的Pass机制实践

例子:

代码语言:javascript代码运行次数:0运行复制

>>> import numpy as np>>> import oneflow as flow>>> m = flow.nn.Dropout(p=0)>>> arr = np.array(...    [...        [-0.7797, 0.2264, 0.2458, 0.4163],...        [0.4299, 0.3626, -0.4892, 0.4141],...        [-1.4115, 1.2183, -0.5503, 0.6520],...    ]... )>>> x = flow.Tensor(arr)>>> y = m(x)>>> y tensor([[-0.7797,  0.2264,  0.2458,  0.4163],        [ 0.4299,  0.3626, -0.4892,  0.4141],        [-1.4115,  1.2183, -0.5503,  0.6520]], dtype=oneflow.float32)

fused_bias_add_mask_scale:fused_bias_add_mask_scale算子需要bias_add算子的输入a和b(bias),然后还需要一个由输入a调用random_mask_like Op产生的掩码Tensor mask作为它的第三个输入,最后还需要bias_add算子的axis属性和Dropout的p属性。

这里需要解释一下为什么需要mask。其实Dropout算子在实现的时候也会产生两个输出,一个是输出Tensor,一个是mask。这是因为Dropout会根据p和我们输入的随机数种子产生一个mask来决定哪些位置的神经元应该保留,哪些位置的神经元置0,为了正确的反向传播的需要我们必须保留这个mask来求取输入Tensor对应的梯度。因此在fused_bias_add_mask_scale Op中,需要将mask显示的传给这个Op,因为这个Op的输出只有一个,不会再输出一个额外的mask了。而这个mask的生成是利用oneflow内部的random_mask_like Op来生成的,这个Op接受一个输入Tensor和p以及一个随机数种子来产生一个具有一定概率分布的掩码Tensor mask。

0x3. Pattern匹配和重写

在了解了这些Op的操作数,属性以及输出之后,我们就可以基于MLIR来做针对BiasAdd和Dropout的Patten自动匹配和重写了。这个功能实现在:https://github.com/Oneflow-Inc/oneflow/pull/7709。

首先,我们需要在oneflow/ir/include/OneFlow/OneFlowPatterns.td这个文件中基于MLIR的DRR框架写出自动匹配和重写的模板,实现如下:

代码语言:javascript代码运行次数:0运行复制

def GetDefaultSeed :  NativeCodeCall;def FusedBiasAddMaskScale :  NativeCodeCall;def IsAddToOutputNone: Constraint<CPred, "">;def FusedBiasAddDropoutPattern : Pattern;

NativeCodeCall是一个占位代码,我们可以通过NativeCodeCall调用我们在Dialect下手写的C++函数。比如:

代码语言:javascript代码运行次数:0运行复制

def GetDefaultSeed :  NativeCodeCall;

这里就调用了我们在OneFlow Dialect下手写的GetDefaultSeed函数,它返回一个OneFlow的DefaultAutoGenerator类生成的随机种子,这个随机种子在Pattern里面作为RandomMaskLikeOp的一个属性被使用:

代码语言:javascript代码运行次数:0运行复制

mlir::IntegerAttr GetDefaultSeed(::mlir::PatternRewriter& rewriter) {  const auto gen = CHECK_JUST(::oneflow::one::DefaultAutoGenerator());  return getSI64IntegerAttr(rewriter, (int64_t)gen->current_seed());}

类似的CreateFusedBiasAddMaskScale这个函数就是将匹配上的Pattern(BiasAddOp+DropoutOp)重写为FusedBiasAddMaskScaleOp。代码实现如下:

代码语言:javascript代码运行次数:0运行复制

::llvm::SmallVector CreateFusedBiasAddMaskScale(::mlir::PatternRewriter& rewriter,                                                                  OpResult dropout_result,                                                                  OpResult bias_add_result,                                                                  Operation* mask) {  if (auto dropout_op = llvm::dyn_cast(dropout_result.getDefiningOp())) {    if (auto bias_add_op = llvm::dyn_cast(bias_add_result.getDefiningOp())) {      SmallVector operands;      operands.push_back(bias_add_op.a());      operands.push_back(bias_add_op.b());      operands.push_back(mask->getResults()[0]);      NamedAttrList fused_bias_add_dropout_attributes = dropout_op->getAttrs();      fused_bias_add_dropout_attributes.append(llvm::StringRef("axis"), bias_add_op.axisAttr());      fused_bias_add_dropout_attributes.append(llvm::StringRef("scale"), dropout_op.rateAttr());      fused_bias_add_dropout_attributes.erase(dropout_op.rateAttrName());      auto res = rewriter                     .create(                         dropout_op->getLoc(), dropout_op->getResultTypes().front(), operands,                         fused_bias_add_dropout_attributes)                     ->getResults();      // bias_add and dropout op is expected to be erased if it is not used      return res;    }  }  return {};}

这个函数接收一个PatternRewriter对象和DropoutOp以及BiasAddOp的输出值,然后从这两个值可以取得定义它们的Op,从Op又可以取得对应的操作数和属性等。然后基于PatternRewriter对象完成创建一个新Op的过程,并在当前DropoutOp的位置完成替换,这样就完成了特定Pattern的重写工作。失效的BiasAddOp和DropoutOp由于是NoSideEffect的,在生成的IR中会自动被删掉。

接下来我们看一下IsAddToOutputNone这个约束,def IsAddToOutputNone: Constraint, "">; 这里使用CPred来自定义了一个约束,这个CPred里面可以放一个任何返回bool类型的C++函数。这里的实现为:

代码语言:javascript代码运行次数:0运行复制

bool IsAddToOutputNone(ValueRange value) { return (int)value.size() > 0 ? false : true; }

即判断Dropout Op的_add_to_output这个可选的输入是否存在,如果不存在才可以使用我们实现的这个Pass。

除了上面的常规部分之外,这里需要注意两个特殊的点,我单独列出。

NativeCodeCall的限制引发的问题

我们可以从MLIR的文档查到NativeCodeCall只能返回一个结果。所以在上面的模板匹配和重写的时候我们给重写的部分设置了2个输出,一个是FusedBiasAddMaskScaleOp的输出(目标输出),一个是使用(replaceWithValue $mask)定义的占位输出。原因是因为Dropout Op有2个输出,如果这里没有定义一个新的占位输出那么这里模板匹配重写时就会报输出个数不一样的错误。这里使用replaceWithValue的原因是它可以简单直接的完成替换一个值(mlir::Value)的功能,比较适合这里的占位作用。

RandomMaskLikeOp为什么要自定义builder

上面的实现中还有一个依赖就是需要自定义RandomMaskLikeOp的builder,新的RandomMaskLikeOp的定义如下:

代码语言:javascript代码运行次数:0运行复制

def OneFlow_RandomMaskLikeOp : OneFlow_BaseOp<"random_mask_like", [NoSideEffect, NoGrad, DeclareOpInterfaceMethods]> {  let input = (ins    OneFlow_Tensor:$like  );  let output = (outs    OneFlow_Tensor:$out  );  let attrs = (ins    DefaultValuedAttr:$rate,    DefaultValuedAttr:$seed  );  let builders = [    OpBuilder  ];  let has_check_fn = 1;  let has_logical_tensor_desc_infer_fn = 1;  let has_physical_tensor_desc_infer_fn = 1;  let has_get_sbp_fn = 1;  let has_data_type_infer_fn = 1;}

自定义builder之后需要将这个builder使用C++来实现一下:

代码语言:javascript代码运行次数:0运行复制

void RandomMaskLikeOp::build(mlir::OpBuilder& odsBuilder, mlir::OperationState& odsState,                             mlir::Value like, StringRef op_name, StringRef device_tag,                             ArrayAttr device_name, IntegerAttr scope_symbol_id,                             ArrayAttr hierarchy, mlir::FloatAttr rate, mlir::IntegerAttr seed) {  odsState.addOperands(like);  odsState.addAttribute(op_nameAttrName(odsState.name), odsBuilder.getStringAttr(op_name));  odsState.addAttribute(device_tagAttrName(odsState.name), odsBuilder.getStringAttr(device_tag));  odsState.addAttribute(device_nameAttrName(odsState.name), device_name);  if (scope_symbol_id) {    odsState.addAttribute(scope_symbol_idAttrName(odsState.name), scope_symbol_id);  }  if (hierarchy) { odsState.addAttribute(hierarchyAttrName(odsState.name), hierarchy); }  odsState.addAttribute(rateAttrName(odsState.name), rate);  odsState.addAttribute(seedAttrName(odsState.name), seed);  odsState.addTypes(like.getType());}

这样做的原因是因为,这里使用RandomMaskLikeOp生成的mask不是要替换的Dag的最外层Op,所以MLIR无法推断RandomMaskLikeOp的输出值类型(如果是单个Op的话,这个Op的输出类型就是它将要replace的那个Op的输出类型),所以我们要提供一种特殊的,不需要输出类型的builder。这个builder会做类型推断,在这个例子就是从like直接取得类型。即:odsState.addTypes(like.getType()); 这行代码。

如果不修改这个就会报类型无法匹配的错误,大概长这样:

代码语言:javascript代码运行次数:0运行复制

python3: /home/xxx/oneflow/build/oneflow/ir/llvm_monorepo-src/mlir/lib/IR/PatternMatch.cpp:328: void mlir::RewriterBase::replaceOpWithResultsOfAnotherOp(mlir::Operation*, mlir::Operation*): Assertion `op->getNumResults() == newOp->getNumResults() && "replacement op doesn't match results of original op"' failed

0x4. 测试

上面就已经讲完了所有的实现细节,我们可以构造一个OneFlow的程序来验证这个IR融合是否正常工作。测试代码如下:

代码语言:javascript代码运行次数:0运行复制

import unittestimport numpy as npimport osos.environ["ONEFLOW_MLIR_ENABLE_ROUND_TRIP"] = "1"import oneflow as flowimport oneflow.unittestdef do_bias_add_dropout_graph(test_case, with_cuda, prob):    x = flow.randn(2, 3, 4, 5)    bias = flow.randn(5)    dropout = flow.nn.Dropout(p=prob)    if with_cuda:        x = x.cuda()        bias = bias.to("cuda")        dropout.to("cuda")    eager_res = dropout(flow._C.bias_add(x, bias, axis=3))    class GraphToRun(flow.nn.Graph):        def __init__(self):            super().__init__()            self.dropout = dropout        def build(self, x, bias):            return self.dropout(flow._C.bias_add(x, bias, axis=3))    graph_to_run = GraphToRun()    lazy_res = graph_to_run(x, bias)    test_case.assertTrue(np.array_equal(eager_res.numpy(), lazy_res.numpy()))@flow.unittest.skip_unless_1n1d()class TestBiasAddDropout(oneflow.unittest.TestCase):    def test_bias_add_dropout_graph(test_case):        do_bias_add_dropout_graph(test_case, True, 1.0)if __name__ == "__main__":    unittest.main()

这里使用了nn.Graph对计算过程进行包装,即使用静态图的模式运行整个程序。nn.Graph被构建之后会生成一个Job(OneFlow的原始计算图表示),然后这个Job会被转换为MLIR表达式(OneFlow Dialect)做上面的Fuse Pass再转回Job(优化后的OneFlow计算图表示)后再做训练或者推理。

我们可以看一下使用MLIR FuseBiasAddDropout Pass前后的IR表示。首先是不使用这个Pass的MLIR表达式:

代码语言:javascript代码运行次数:0运行复制

module {  oneflow.job @GraphToRun_0(%arg0: tensor, %arg1: tensor) -> tensor {    %output = "oneflow.input"(%arg0) {data_type = 2 : i32, device_name = ["@0:0"], device_tag = "gpu", hierarchy = [1], is_dynamic = false, nd_sbp = ["B"], op_name = "_GraphToRun_0_input.0.0_2", output_lbns = ["_GraphToRun_0_input.0.0_2/out"], scope_symbol_id = 4611686018427420671 : i64, shape = [2 : si64, 3 : si64, 4 : si64, 5 : si64]} : (tensor) -> tensor    %output_0 = "oneflow.input"(%arg1) {data_type = 2 : i32, device_name = ["@0:0"], device_tag = "gpu", hierarchy = [1], is_dynamic = false, nd_sbp = ["B"], op_name = "_GraphToRun_0_input.0.1_3", output_lbns = ["_GraphToRun_0_input.0.1_3/out"], scope_symbol_id = 4611686018427420671 : i64, shape = [5 : si64]} : (tensor) -> tensor    %0 = "oneflow.bias_add"(%output, %output_0) {axis = 3 : si32, device_name = ["@0:0"], device_tag = "gpu", hierarchy = [1], op_name = "bias_add-0", output_lbns = ["bias_add-0/out_0"], scope_symbol_id = 4611686018427420671 : i64} : (tensor, tensor) -> tensor    %out, %mask = "oneflow.dropout"(%0) {device_name = ["@0:0"], device_tag = "gpu", hierarchy = [1], op_name = "dropout-dropout-1", output_lbns = ["dropout-dropout-1/out_0", "dropout-dropout-1/mask_0"], rate = 1.000000e+00 : f32, scope_symbol_id = 4611686018427428863 : i64} : (tensor) -> (tensor, tensor)    %output_1 = "oneflow.output"(%out) {data_type = 2 : i32, device_name = ["@0:0"], device_tag = "gpu", hierarchy = [1], is_dynamic = false, nd_sbp = ["B"], op_name = "_GraphToRun_0_output.0.0_2", output_lbns = ["_GraphToRun_0_output.0.0_2/out"], scope_symbol_id = 4611686018427420671 : i64, shape = [2 : si64, 3 : si64, 4 : si64, 5 : si64]} : (tensor) -> tensor    oneflow.return %output_1 : tensor  }}

然后是启动这个Pass之后获得的MLIR表达式:

代码语言:javascript代码运行次数:0运行复制

module {  oneflow.job @GraphToRun_0(%arg0: tensor, %arg1: tensor) -> tensor {    %output = "oneflow.input"(%arg0) {data_type = 2 : i32, device_name = ["@0:0"], device_tag = "gpu", hierarchy = [1], is_dynamic = false, nd_sbp = ["B"], op_name = "_GraphToRun_0_input.0.0_2", output_lbns = ["_GraphToRun_0_input.0.0_2/out"], scope_symbol_id = 4611686018427420671 : i64, shape = [2 : si64, 3 : si64, 4 : si64, 5 : si64]} : (tensor) -> tensor    %output_0 = "oneflow.input"(%arg1) {data_type = 2 : i32, device_name = ["@0:0"], device_tag = "gpu", hierarchy = [1], is_dynamic = false, nd_sbp = ["B"], op_name = "_GraphToRun_0_input.0.1_3", output_lbns = ["_GraphToRun_0_input.0.1_3/out"], scope_symbol_id = 4611686018427420671 : i64, shape = [5 : si64]} : (tensor) -> tensor    %0 = "oneflow.random_mask_like"(%output) {device_name = ["@0:0"], device_tag = "gpu", hierarchy = [1], op_name = "bias_add-0", rate = 1.000000e+00 : f32, scope_symbol_id = 4611686018427428863 : i64, seed = 4920936260932536 : si64} : (tensor) -> tensor    %1 = "oneflow.fused_bias_add_mask_scale"(%output, %output_0, %0) {axis = 3 : si32, device_name = ["@0:0"], device_tag = "gpu", hierarchy = [1], op_name = "dropout-dropout-1", output_lbns = ["dropout-dropout-1/out_0", "dropout-dropout-1/mask_0"], scale = 1.000000e+00 : f32, scope_symbol_id = 4611686018427428863 : i64} : (tensor, tensor, tensor) -> tensor    %output_1 = "oneflow.output"(%1) {data_type = 2 : i32, device_name = ["@0:0"], device_tag = "gpu", hierarchy = [1], is_dynamic = false, nd_sbp = ["B"], op_name = "_GraphToRun_0_output.0.0_2", output_lbns = ["_GraphToRun_0_output.0.0_2/out"], scope_symbol_id = 4611686018427420671 : i64, shape = [2 : si64, 3 : si64, 4 : si64, 5 : si64]} : (tensor) -> tensor    oneflow.return %output_1 : tensor  }}

可以看到上面实现的FuseBiasAddDropout Pass成功完成了BiasAdd和Dropout Op的融合。

0x5. 总结

这篇文章介绍了MLIR的Pass机制的实践,在OneFlow Dialect中已经实现了很多常用的Fuse Op并且使用MLIR来做Pattern Match和Rewrite,从而在不需要用户修改任何代码的情况下无感加速计算图以及节省显存。如果你对这部分很感兴趣,可以到我们的OneFlow仓库中查看。

0x6. 资料https://github.com/Oneflow-Inc/oneflowhttps://mlir.llvm.org/docs/DeclarativeRewrites/

以上就是【从零开始学深度学习编译器】十九,MLIR的Pass机制实践的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/11262.html

赞 (0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
河南crm客户管理系统哪个好用
上一篇 2025年11月15日 17:22:34
客户管理工具哪个好
下一篇 2025年11月15日 17:22:49

相关推荐

  • 荣耀Magic8和新平板官宣 首批搭载第五代骁龙8至尊版

    荣耀Magic8和新平板官宣 首批搭载第五代骁龙8至尊版荣耀Magic8和新平板官宣 首批搭载第五代骁龙8至尊版荣耀Magic8和新平板官宣 首批搭载第五代骁龙8至尊版荣耀Magic8和新平板官宣 首批搭载第五代骁龙8至尊版

    9月25日,cnmo获悉,荣耀手机官方正式发布消息,宣布荣耀magic8系列以及荣耀magicpad3 pro平板将率先搭载高通最新推出的第五代骁龙8至尊版移动平台。 荣耀Magic7 Pro 此前多方爆料显示,荣耀Magic8系列将配备超过7000mAh的大容量电池,支持100W有线快充和80W无…

    2026年9月28日 • 用户投稿
    100
  • sublime怎么在侧边栏中隐藏特定的文件类型_侧边栏文件过滤设置

    sublime怎么在侧边栏中隐藏特定的文件类型_侧边栏文件过滤设置sublime怎么在侧边栏中隐藏特定的文件类型_侧边栏文件过滤设置sublime怎么在侧边栏中隐藏特定的文件类型_侧边栏文件过滤设置sublime怎么在侧边栏中隐藏特定的文件类型_侧边栏文件过滤设置

    要隐藏Sublime Text侧边栏中的特定文件类型,需修改用户或项目设置中的”folder_exclude_patterns”和”file_exclude_patterns”数组。首先在全局设置中添加如”.git”、&#822…

    2026年9月28日 • 用户投稿
    000
  • 使用单个循环优化 Java 代码:替代多个循环的策略

    使用单个循环优化 Java 代码:替代多个循环的策略使用单个循环优化 Java 代码:替代多个循环的策略使用单个循环优化 Java 代码:替代多个循环的策略使用单个循环优化 Java 代码:替代多个循环的策略

    本文旨在帮助开发者优化 Java 代码,特别是当遇到需要多次遍历同一数据集以查找不同类型数据时。我们将探讨如何使用单个循环和标志变量来替代多个循环,从而提高代码的效率和可读性,并提供多种优化策略,包括使用布尔标志、数组和辅助类,以及性能考量。 在处理数据时,经常会遇到需要从同一数据集中提取不同类型的…

    2026年9月28日 • 用户投稿
    000
  • Linux文件系统架构详解

    Linux文件系统架构详解Linux文件系统架构详解Linux文件系统架构详解Linux文件系统架构详解

    Linux文件系统架构详解 Linux作为一款开源的操作系统,其文件系统架构扮演着至关重要的角色。文件系统是操作系统中负责管理数据的重要部分,Linux系统中有多种不同的文件系统,每种文件系统都有其特定的优势和用途。在本文中,我们将详细解析Linux文件系统的架构,从文件系统的基本概念、目录结构到具…

    2026年9月28日 • 用户投稿
    700
  • sublime怎么设置字体大小和样式_Sublime字体大小及样式配置方法

    sublime怎么设置字体大小和样式_Sublime字体大小及样式配置方法sublime怎么设置字体大小和样式_Sublime字体大小及样式配置方法sublime怎么设置字体大小和样式_Sublime字体大小及样式配置方法sublime怎么设置字体大小和样式_Sublime字体大小及样式配置方法

    调整Sublime Text字体大小和样式需修改用户设置文件,通过添加或修改font_size和font_face实现个性化配置,保存后实时生效。1. 打开Preferences -> Settings,编辑右侧用户设置;2. 添加”font_size”: 14、&#8…

    2026年9月28日 • 用户投稿
    100
  • Centos7.3环境下安装最新版的Python3.8.4

    Centos7.3环境下安装最新版的Python3.8.4Centos7.3环境下安装最新版的Python3.8.4Centos7.3环境下安装最新版的Python3.8.4Centos7.3环境下安装最新版的Python3.8.4

    在centos 7.3环境下安装最新版python 3.8.4的步骤如下: 首先,退出Python命令行的方法包括: 输入exit()并按回车键输入quit()并按回车键直接按Ctrl+Z 接下来,前往Python的官方网站下载Python包。 立即学习“Python免费学习笔记(深入)”; 每个版…

    2026年9月28日 • 用户投稿
    100
  • 如何在mysql中创建外键索引

    创建表时定义外键会自动创建索引,如CREATE TABLE orders含FOREIGN KEY(user_id)则user_id自动索引;2. 已有表添加外键前需先手动建索引,如CREATE INDEX idx_user_id ON orders(user_id),再ALTER TABLE加外键约…

    2026年9月28日
    300
  • 夸克AI最新官方主页地址 夸克AI人工智能助手直达入口链接

    夸克AI最新官方主页地址是https://www.quark.cn/,提供AI搜索、文档处理、云端存储及多端协同服务,支持格式转换、内容生成与智能摘要功能。 ☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜ 夸克AI最新官方主页地址在哪里?这是…

    2026年9月28日
    000
  • linux mint哪个版本好

    linux mint哪个版本好linux mint哪个版本好linux mint哪个版本好linux mint哪个版本好

    最佳的 Linux Mint 版本取决于个人偏好和用例,但对于大多数用户而言,Linux Mint Cinnamon 是一个不错的选择,因为它提供了易于使用且功能丰富的桌面体验。其他版本包括:MATE(轻量级)、Xfce(更轻量级)、KDE(功能丰富且可定制)和 LXDE(极轻量级)。 Linux …

    2026年9月28日 • 用户投稿
    000
  • 虚拟机安装以及PCL的配置(1)

    虚拟机安装以及PCL的配置(1)虚拟机安装以及PCL的配置(1)虚拟机安装以及PCL的配置(1)虚拟机安装以及PCL的配置(1)

    在windows系统下安装虚拟机的步骤如下(这些步骤同样适用于在虚拟机中配置ubuntu系统或双系统配置pcl环境): (1) 下载VMware并进行安装(可以通过百度搜索找到多个可供下载的资源)。 (2) 安装步骤: 双击下载的安装文件,按照提示点击“下一步”,无需更改默认安装路径(当然你也可以选…

    2026年9月28日 • 用户投稿
    200
  • 续航一般多久够用?友望洗地机:长续航+强性能,全屋清洁“一劳永逸”

    续航一般多久够用?友望洗地机:长续航+强性能,全屋清洁“一劳永逸”续航一般多久够用?友望洗地机:长续航+强性能,全屋清洁“一劳永逸”续航一般多久够用?友望洗地机:长续航+强性能,全屋清洁“一劳永逸”续航一般多久够用?友望洗地机:长续航+强性能,全屋清洁“一劳永逸”

    在家庭清洁场景中,洗地机凭借高效省力的特点,正逐步成为现代家庭的清洁“主力军”。然而面对琳琅满目的产品型号,消费者仍有不少疑问:洗地机究竟适合多大面积的空间?选购时应重点关注哪些功能?续航时间多久才够用?今天,我们将从真实用户需求出发,结合友望最新推出的大头pro洗地机,深入解析这些常见问题。 一、…

    2026年9月28日 • 用户投稿
    200
  • 如何在 Android 中保存动态创建的复选框状态

    如何在 Android 中保存动态创建的复选框状态如何在 Android 中保存动态创建的复选框状态如何在 Android 中保存动态创建的复选框状态如何在 Android 中保存动态创建的复选框状态

    本文介绍了如何在 Android 应用中保存动态创建的复选框的状态,以便用户在重新打开应用或界面后,复选框的选中状态能够保持不变。我们将探讨使用 SharedPreferences 来持久化复选框状态的方法,并提供示例代码帮助你理解和实现。 使用 SharedPreferences 持久化复选框状态…

    2026年9月28日 • 用户投稿
    000
  • sublime怎么合并多行为一行_Sublime多行内容合并为单行操作

    sublime怎么合并多行为一行_Sublime多行内容合并为单行操作sublime怎么合并多行为一行_Sublime多行内容合并为单行操作sublime怎么合并多行为一行_Sublime多行内容合并为单行操作sublime怎么合并多行为一行_Sublime多行内容合并为单行操作

    答案:Sublime Text中合并多行可通过三种方法实现。1. 使用查找替换功能,结合正则表达式r?n匹配换行符,替换为指定分隔符;2. 手动选中多行后删除换行符并添加分隔符;3. 使用内置“Join Lines”命令,快捷键Ctrl+J(Windows/Linux)或Cmd+J(Mac),自动以…

    2026年9月28日 • 用户投稿
    100
  • 如何在Android中保存动态创建的CheckBox的状态

    如何在Android中保存动态创建的CheckBox的状态如何在Android中保存动态创建的CheckBox的状态如何在Android中保存动态创建的CheckBox的状态如何在Android中保存动态创建的CheckBox的状态

    本文旨在帮助开发者解决在Android应用中动态创建的CheckBox的状态保存问题。通过利用Shared Preferences,我们可以有效地存储CheckBox的选中状态,确保用户在重新进入应用或页面时,CheckBox的状态能够被正确恢复,从而提供更佳的用户体验。本文将提供详细的步骤和示例代…

    2026年9月28日 • 用户投稿
    100
  • 第五代高通骁龙8至尊版正式发布:全球最快移动SoC

    第五代高通骁龙8至尊版正式发布:全球最快移动SoC第五代高通骁龙8至尊版正式发布:全球最快移动SoC第五代高通骁龙8至尊版正式发布:全球最快移动SoC第五代高通骁龙8至尊版正式发布:全球最快移动SoC

    在今日举行的骁龙峰会上,高通正式发布了其最新旗舰移动平台——第五代骁龙 8 至尊版(Snapdragon 8 Elite Gen 5),并宣称该芯片为“全球速度最快的移动 SoC”。 此次发布的芯片基于台积电最新的第三代3nm N3P工艺打造,在CPU架构上采用了全新的Oryon核心设计,延续了2+…

    2026年9月28日 • 用户投稿
    000
  • Perplexity AI比Google好吗 与传统搜索引擎对比

    Perplexity AI比Google好吗 与传统搜索引擎对比Perplexity AI比Google好吗 与传统搜索引擎对比Perplexity AI比Google好吗 与传统搜索引擎对比Perplexity AI比Google好吗 与传统搜索引擎对比

    perplexity ai 的最大优势在于对话式搜索与实时检索的结合,能自然理解提问意图并提供结构化答案,适合快速获取信息;2. google 在全面性、稳定性与权威性方面仍占优势,适合深度调研和查找权威资料;3. 两者使用体验各有侧重,perplexity ai 提升效率,google 保障内容深…

    2026年9月28日 • 用户投稿
    100
  • linux中atq命令如何使用

    linux中atq命令如何使用linux中atq命令如何使用linux中atq命令如何使用linux中atq命令如何使用

    atq 可以在 Linux 中显示计划运行的作业队列。具体使用方式如下:查看所有已计划作业:atq查看特定用户已计划作业:atq -u 用户名查看作业的命令行:atq -c 作业号查看作业的队列信息:atq -q 作业号查看作业的详细信息:atq -V 作业号 Linux 中 atq 命令的使用 a…

    2026年9月28日 • 用户投稿
    000
  • Java中ArrayList引用传递陷阱:避免数据意外修改的策略

    Java中ArrayList引用传递陷阱:避免数据意外修改的策略Java中ArrayList引用传递陷阱:避免数据意外修改的策略Java中ArrayList引用传递陷阱:避免数据意外修改的策略Java中ArrayList引用传递陷阱:避免数据意外修改的策略

    本文探讨了Java中ArrayList作为引用类型在对象构造时可能导致的数据意外修改问题。当将同一个ArrayList实例传递给多个对象后,对该列表的后续操作(如清空或添加元素)会影响所有引用它的对象。核心解决方案是为每个需要独立数据副本的对象,实例化一个新的ArrayList,从而确保数据隔离和一…

    2026年9月28日 • 用户投稿
    000
  • 永久免费的linux服务器怎么搭建

    永久免费的linux服务器怎么搭建永久免费的linux服务器怎么搭建永久免费的linux服务器怎么搭建永久免费的linux服务器怎么搭建

    搭建永久免费的 Linux 服务器有两种方法:利用云服务商的免费试用或永久免费计划使用开源云平台(例如 DigitalOcean 或 Linode)提供的永久免费实例 搭建永久免费的 Linux 服务器 如何搭建永久免费的 Linux 服务器? 搭建永久免费的 Linux 服务器主要有以下两种方法:…

    2026年9月28日 • 用户投稿
    300
  • sublime怎么处理gbk编码的文件不乱码_Sublime正确打开GBK编码文件不乱码的设置

    sublime怎么处理gbk编码的文件不乱码_Sublime正确打开GBK编码文件不乱码的设置sublime怎么处理gbk编码的文件不乱码_Sublime正确打开GBK编码文件不乱码的设置sublime怎么处理gbk编码的文件不乱码_Sublime正确打开GBK编码文件不乱码的设置sublime怎么处理gbk编码的文件不乱码_Sublime正确打开GBK编码文件不乱码的设置

    安装ConvertToUTF8插件可解决Sublime Text打开GBK文件乱码问题,该插件能自动识别并转换编码,确保文件正确显示且保存时保留原编码,同时建议设置默认编码为UTF-8、备用编码为GBK,并通过项目配置或团队规范统一编码,避免后续乱码。 Sublime Text在处理GBK编码文件时…

    2026年9月28日 • 用户投稿
    100

发表回复

登录后才能评论
关注微信