0

0

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

爱谁谁

爱谁谁

发布时间:2025-07-20 11:22:11

|

786人浏览过

|

来源于php中文网

原创

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节

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

Megatron 4.2节

这一节编译优化讲的主要是可以通过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算子的输入ab(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<"mlir::oneflow::GetDefaultSeed($_builder)">;def FusedBiasAddMaskScale :  NativeCodeCall<"mlir::oneflow::CreateFusedBiasAddMaskScale($_builder, $0, $1, $2)">;def IsAddToOutputNone: Constraint, "">;def FusedBiasAddDropoutPattern : Pattern<  (    OneFlow_DropoutOp: $dropout_res    (      OneFlow_BiasAddOp: $bias_add_res        $a,        $b,        $bias_add_op_name,        $bias_add_device_tag,        $bias_add_device_name,        $bias_add_scope_symbol_id,        $bias_add_hierarchy,        $bias_add_op_axis    ),    $_add_to_output,    $dropout_op_name,    $dropout_device_tag,    $dropout_device_name,    $dropout_scope_symbol_id,    $dropout_hierarchy,    $dropout_op_rate  ),  [    (      FusedBiasAddMaskScale      $dropout_res__0,      $bias_add_res,      (        OneFlow_RandomMaskLikeOp : $mask          $a,          $bias_add_op_name,          $dropout_device_tag,          $dropout_device_name,          $dropout_scope_symbol_id,          $dropout_hierarchy,          $dropout_op_rate,          (GetDefaultSeed)      )    ),    (replaceWithValue $mask)  ],  [(IsAddToOutputNone $_add_to_output)]>;

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

代码语言:javascript代码运行次数:0运行复制
def GetDefaultSeed :  NativeCodeCall<"mlir::oneflow::GetDefaultSeed($_builder)">;

这里就调用了我们在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。代码实现如下:

Sora
Sora

Sora是OpenAI发布的一种文生视频AI大模型,可以根据文本指令创建现实和富有想象力的场景。

下载
代码语言:javascript代码运行次数:0运行复制
::llvm::SmallVector<::mlir::Value, 4> 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<(ins      "Value":$like,      "StringRef":$op_name,      "StringRef":$device_tag,      "ArrayAttr":$device_name,      "IntegerAttr":$scope_symbol_id,      "ArrayAttr":$hierarchy,      "FloatAttr":$rate,      "IntegerAttr":$seed    )>  ];  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<2x3x4x5xf32>, %arg1: tensor<5xf32>) -> tensor<2x3x4x5xf32> {    %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<2x3x4x5xf32>) -> tensor<2x3x4x5xf32>    %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<5xf32>) -> tensor<5xf32>    %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<2x3x4x5xf32>, tensor<5xf32>) -> tensor<2x3x4x5xf32>    %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<2x3x4x5xf32>) -> (tensor<2x3x4x5xf32>, tensor<2x3x4x5xi8>)    %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<2x3x4x5xf32>) -> tensor<2x3x4x5xf32>    oneflow.return %output_1 : tensor<2x3x4x5xf32>  }}

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

代码语言:javascript代码运行次数:0运行复制
module {  oneflow.job @GraphToRun_0(%arg0: tensor<2x3x4x5xf32>, %arg1: tensor<5xf32>) -> tensor<2x3x4x5xf32> {    %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<2x3x4x5xf32>) -> tensor<2x3x4x5xf32>    %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<5xf32>) -> tensor<5xf32>    %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<2x3x4x5xf32>) -> tensor<2x3x4x5xf32>    %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<2x3x4x5xf32>, tensor<5xf32>, tensor<2x3x4x5xf32>) -> tensor<2x3x4x5xf32>    %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<2x3x4x5xf32>) -> tensor<2x3x4x5xf32>    oneflow.return %output_1 : tensor<2x3x4x5xf32>  }}

可以看到上面实现的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/

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

556

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

732

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

477

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

414

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

991

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

658

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

552

2023.09.20

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

72

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 7.4万人学习

Git 教程
Git 教程

共21课时 | 2.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号