尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

CANN推理配置解析:YAML如何驱动AscendCL运行时

发布时间:2026/9/16 3:38:21

资讯中心
01
ARTICLE

CANN推理配置解析:YAML如何驱动AscendCL运行时

CANN推理配置解析:YAML如何驱动AscendCL运行时
1. 这不是配置文件解析而是运行时数据流的“心脏起搏器”你打开cann-recipes-infer的源码看到一堆 YAML 文件散落在configs/目录下yolov5s.yaml、resnet50.yaml、atlas200dk.yaml……第一反应可能是“哦这不就是个参数表嘛读进来塞进模型里就完事了”——我最初也这么想。但当你真正把断点打在infer.py的main()函数入口一路跟下去会发现 YAML 并非简单地被“加载”后就扔进垃圾桶它像一条精密的血管从磁盘上的静态文本经过层层解包、校验、映射、转换最终成为运行时内存中活生生的 Python 对象图直接驱动推理引擎的每一个决策节点张量形状怎么分配、算子怎么调度、NPU 芯片资源怎么切分、预处理 pipeline 怎么串联。YAML 在这里不是配置的终点而是运行时逻辑的起点。它解决的核心问题是把人类可读的声明式描述比如input_shape: [1, 3, 640, 640]无损、无歧义、可验证地翻译成 CANN 运行时AscendCL能理解的二进制指令流与内存布局。这背后没有魔法只有三道硬核工序Schema 驱动的结构化加载、上下文感知的动态补全、以及与 AscendCL API 的零拷贝绑定。如果你正在调试cann-recipes-infer里某个模型跑不通、精度掉点、或者 NPU 利用率上不去十有八九问题就藏在 YAML 解析后的那个Config对象里——它可能漏掉了某个aclrtSetDevice的显式设备号也可能把precision_mode错配成了allow_fp32_to_fp16而非force_fp16。这篇笔记不讲 YAML 语法基础网上一搜一大把只聚焦一个实战命题当yaml.load()返回的那个字典如何一步步蜕变成aclrtCreateContext()所依赖的、带着血肉温度的运行时实例适合所有正在用 CANN 做实际部署的工程师、算法同学以及那些被aclError_t错误码折磨得夜不能寐的现场支持人员。2. 整体设计思路为什么不用 JSON 或 INIYAML 的不可替代性2.1 不是“选了 YAML”而是“被 YAML 选中”很多人以为cann-recipes-infer用 YAML 是因为“看着顺眼”或“社区流行”。错。这是 CANN 工具链在工程实践中反复踩坑后对配置表达力、可维护性、与硬件特性耦合度三者权衡出的唯一解。我们来拆解这个决策背后的硬逻辑首先看JSON。它结构清晰、解析快但致命缺陷是无法表达注释和锚点引用。在cann-recipes-infer的典型场景里一个yolov5s.yaml可能需要复用common_preprocess.yaml中定义的归一化参数mean: [0.485, 0.456, 0.406]同时又要覆盖atlas200dk.yaml中指定的 NPU 设备 IDdevice_id: 0。JSON 没法做这种“继承覆盖”的组合你只能复制粘贴一旦common_preprocess.yaml更新所有下游 YAML 都得手动改——这在上百个模型配置的产线环境中等于埋下定时炸弹。而 YAML 的anchor和*anchor机制让yolov5s.yaml可以这样写preprocess: common_preprocess mean: [0.485, 0.456, 0.406] std: [0.229, 0.224, 0.225] resize: [640, 640] hardware: atlas200dk device_id: 0 precision_mode: force_fp16 model: : *common_preprocess : *atlas200dk name: yolov5s这行: *common_preprocess不是语法糖它是PyYAML解析器在SafeLoader阶段就完成的深度合并生成的字典天然具备嵌套继承关系后续代码无需额外写“合并逻辑”。再看INI。它轻量但缺乏层级嵌套能力。CANN 的配置不是扁平的键值对而是树状结构model - backbone - layer_depth、runtime - memory - buffer_pool_size、preprocess - augment - mosaic_prob。INI 的section只能做一级分组要表达三层嵌套就得写成model.backbone.layer_depth 50这不仅破坏可读性更让 Schema 校验变得极其脆弱——一个拼写错误model.backbome.layer_depth就会静默失效而 YAML 的缩进强制结构让这种错误在解析阶段就被yaml.scanner.ScannerError拦住。最后看环境变量或命令行参数。它们适合覆盖单个值如--device_id 1但面对cann-recipes-infer动辄 30 个强关联参数输入尺寸、精度模式、内存池大小、预处理链、后处理阈值、NPU 核心数绑定……命令行会膨胀到无法维护。更重要的是环境变量无法表达条件分支。比如atlas200dk和atlas300i的memory配置差异极大前者需要buffer_pool_size: 268435456256MB后者则需buffer_pool_size: 536870912512MB。YAML 的!include和!env标签配合ruamel.yaml扩展能让你在hardware.yaml里写%YAML 1.2 --- device_type: !env ATLAS_DEVICE atlas200dk memory: : !include memory_${device_type}.yaml启动时export ATLAS_DEVICEatlas300i就能自动加载memory_atlas300i.yaml。这种基于环境的动态装配能力是其他格式望尘莫及的。所以YAML 在cann-recipes-infer里不是“配置格式”而是一种轻量级的、带元编程能力的领域特定语言DSL。它的价值不在语法本身而在PyYAMLruamel.yaml 自定义 Loader 构建出的可扩展解析管道。这个管道的设计哲学是一切配置必须可验证、可追溯、可组合、可审计。你看到的.yaml文件本质是给 CANN 运行时写的“需求说明书”而解析器就是那个逐字逐句审阅说明书、并把它翻译成机器指令的严谨工程师。2.2 三层解析架构从文本到运行时对象的蜕变路径cann-recipes-infer的 YAML 解析不是yaml.load()一锤定音而是一个精心设计的三层流水线每一层都承担明确职责且彼此解耦第一层安全加载Safe Loading位于utils/config_loader.py核心是SafeYamlLoader类。它继承自ruamel.yaml.CLoader但重写了construct_mapping()方法禁用所有危险的构造器如!!python/object防止恶意 YAML 注入执行任意代码。更重要的是它强制启用preserve_quotesTrue和allow_duplicate_keysFalse。前者确保字符串中的引号如model_path: /home/ascend/model/yolov5s.om不被意外剥离避免路径解析错误后者让key: value重复定义直接报错杜绝配置冲突。这一层输出的是一个原始的、带位置信息的CommentedMap对象——注意不是普通dict而是ruamel.yaml提供的增强类型它内部记录了每个 key-value 在源文件中的行号、列号为后续错误定位提供精准坐标。第二层Schema 验证与结构化Schema-Driven Validation位于schemas/目录每个 YAML 类型都有对应的 JSON Schema 文件如model_schema.json、runtime_schema.json。解析流程调用jsonschema.validate()但做了关键增强Schema 不是静态校验而是动态生成。例如model_schema.json中input_shape字段的type定义为array但items的minItems和maxItems并非硬编码而是从cann_version环境变量中读取当前 CANN 版本如6.3.RC1再查version_compatibility.json映射表确定该版本支持的最小/最大维度数CANN 6.0 支持 4D6.3 新增 5D 支持。这意味着同一个yolov5s.yaml在 CANN 6.0 下解析会因input_shape: [1,3,640,640]4D通过在 CANN 5.1 下却会因版本不兼容而失败并给出精确提示“input_shaperequires CANN 6.0, current version is 5.1.2”。这种版本感知的 Schema让配置错误从“运行时报错”提前到“加载时报错”大幅缩短调试周期。第三层上下文注入与运行时绑定Context-Aware Binding这是最核心的一层位于core/config.py的Config类。它接收前两层输出的CommentedMap不做简单封装而是执行三项关键操作动态补全Dynamic Completion检查hardware.device_id是否为空若为空则调用get_available_devices()查询系统中所有可用 NPU 设备自动填入第一个device_id: 0并记录日志INFO: Auto-completed device_id to 0 (available: [0,1])。这避免了新手因忘记填device_id导致aclrtSetDevice失败。路径解析Path Resolution将所有*.om、*.bin、*.txt等路径字段从相对路径如model_path: models/yolov5s.om转换为绝对路径/home/ascend/cann-recipes-infer/models/yolov5s.om并验证文件是否存在、是否可读。如果不存在抛出ConfigError并附带完整路径和ls -l命令建议。API 绑定API Binding将Config实例的属性直接映射到 AscendCL 的 C 接口参数。例如config.runtime.precision_mode的值会被Config.to_acl_config()方法转换为acl::AclConfig结构体的precisionMode字段其值不是字符串force_fp16而是ACL_FP16枚举常量。这个转换过程包含严格校验如果precision_mode值不在预定义枚举列表中立即报错而非静默忽略。这三层架构共同构成了一个可审计、可回溯、可版本控制的配置生命周期管理器。它确保了从你编辑 YAML 文件的那一刻起到aclrtCreateContext()成功返回的那一刻每一步都是确定性的、可验证的、有据可查的。这不是炫技而是工业级部署对可靠性的基本要求。3. 核心细节解析YAML 字段如何影响 CANN 运行时行为3.1model模块OM 模型加载与输入输出张量的“契约”model部分是整个配置的基石它定义了模型文件、输入输出规范这些字段直接决定aclrtLoadModel()的行为和后续张量内存的分配策略。我们逐字段深挖其运行时含义model_path这不是一个简单的字符串。cann-recipes-infer在解析时会执行os.path.abspath()os.path.expanduser()确保路径标准化。更重要的是它会调用acl.rt.get_model_desc(model_path)底层是aclGetModelDescC API读取 OM 模型的元数据。这个元数据包含模型的input_num、output_num、每个输入/输出的name、dims、data_type。如果model_path指向的文件不是有效 OM 格式或aclGetModelDesc返回ACL_ERROR_INVALID_PARAM解析会在此刻失败错误信息精确到字节偏移Invalid OM header at offset 0x1A。这比等到aclrtLoadModel()时才报错节省了至少 2 秒的加载时间。input_shape与input_name这两个字段构成了一组强约束。input_name必须与 OM 模型元数据中input[0].name完全一致区分大小写、空格。input_shape的维度数len(input_shape)必须等于input[0].dims.size()且每个维度值必须匹配input_shape[i] input[0].dims[i]。如果不匹配cann-recipes-infer不会尝试 reshape而是直接报错Input shape mismatch: expected [1,3,640,640], got [1,3,416,416]。这是因为 CANN 的aclrtCreateTensorDesc()创建的张量描述符其dims是只读的一旦创建无法修改。强行 mismatch 会导致aclrtMalloc()分配的内存与模型期望的 tensor size 不符引发后续aclrtMemcpyHtoDAsync()的越界写入后果是 NPU 硬件异常重启。output_names这是一个列表顺序至关重要。cann-recipes-infer在aclrtCreateModelDesc()后会遍历output_names对每个 name 调用aclrtGetOutputTensorDescByIndex()获取对应索引的输出张量描述符。如果output_names中的某个 name 在 OM 元数据中找不到解析失败如果顺序错乱比如把boxes放在scores前面而模型实际输出是scores在前那么后续aclrtGetModelOutputSizeByIndex(0)获取的 size 就会错配导致aclrtMalloc()分配的内存不足以容纳boxes数据结果是aclrtMemcpyDtoHAsync()读取时发生段错误Segmentation fault。我在 Atlas 200 DK 上调试 YOLOv5 时就遇到过这个坑output_names: [boxes, scores, classes]写成了[scores, boxes, classes]现象是boxes数据全是 0scores数据正常——因为scores的内存被boxes占用了。dynamic_batch_size这个字段开启了 CANN 的动态批处理能力。当设为true时cann-recipes-infer会调用aclrtSetDynamicBatchSize()并将input_shape[0]视为最大 batch size。运行时你可以用aclrtSetDynamicBatchSize()动态调整实际 batch但必须 ≤input_shape[0]。如果设为false则input_shape[0]就是固定 batch size任何运行时的 batch 变更都会被拒绝。这个开关直接影响内存池的大小计算动态 batch 模式下buffer_pool_size需按最大 batch 计算固定 batch 模式下可按实际 batch 计算节省内存。提示dynamic_batch_size: true时input_shape的第一个维度batch必须是1或-1表示动态不能是具体数字如4。否则aclrtSetDynamicBatchSize()会返回ACL_ERROR_INVALID_PARAMETER。3.2runtime模块NPU 资源调度的“宪法”runtime部分是 CANN 运行时的中枢神经它决定了 NPU 芯片如何被使用、内存如何被管理、精度如何被控制。这里的每个字段都对应着一个底层 C API 的调用。device_id这是aclrtSetDevice(device_id)的直接参数。cann-recipes-infer在Config初始化时会先调用acl.rt.get_device_count()获取可用设备数然后验证device_id是否在[0, count-1]范围内。如果device_id超出范围报错Device ID {device_id} not available, available: [0-{count-1}]。更关键的是它会检查该设备是否已被其他进程占用。通过读取/proc/sys/dev/ascend/ai_core/device/{device_id}/statusLinux sysfs 接口如果状态为busy则拒绝启动并提示Device {device_id} is busy by another process, please check with npu-smi info。这避免了多个 infer 进程争抢同一 NPU 设备导致的ACL_ERROR_RESOURCE_BUSY。precision_mode这个字段的值allow_fp32_to_fp16,force_fp16,must_keep_origin_dtype会直接映射到acl::AclConfig::precisionMode。force_fp16是最常用的选择它告诉 CANN 运行时所有 FP32 计算都必须转换为 FP16 执行。这不仅仅是精度损失的问题更是性能问题。Atlas 系列 NPU 的 FP16 算力是 FP32 的 2 倍force_fp16能让卷积、矩阵乘等核心算子跑满硬件峰值。但如果模型中有BatchNorm层其running_mean和running_var参数是 FP32 的force_fp16会导致数值不稳定精度下降。此时allow_fp32_to_fp16更合适它允许运行时自动选择最优精度对BatchNorm保持 FP32对卷积使用 FP16。cann-recipes-infer的Config类会根据model.name如resnet50自动推荐默认precision_mode并在日志中提示INFO: Recommended precision_mode for resnet50 is allow_fp32_to_fp16 based on model architecture。memory子模块这是最容易被低估的部分。buffer_pool_size和workspace_size不是随意填写的数字而是需要精确计算的。buffer_pool_size这是aclrtMalloc()分配的连续内存池大小用于存放模型权重、中间特征图、输入输出张量。计算公式为buffer_pool_size weight_size max_feature_map_size * 2 input_tensor_size output_tensor_size其中weight_size从 OM 文件头读取max_feature_map_size是模型所有层中最大的 feature map sizeH*W*C*bytes_per_elementcann-recipes-infer会解析 OM 的model_desc自动计算input_tensor_size和output_tensor_size由input_shape和output_shapes计算得出。如果buffer_pool_size设置过小aclrtMalloc()会返回ACL_ERROR_NOT_ENOUGH_MEMORY过大则浪费内存影响多实例并发。workspace_size这是aclrtCreateContext()创建上下文时为 AscendCL 运行时内部工作区分配的内存。它不用于用户数据而是用于算子调度、任务队列、DMA 控制等。其大小与device_id和cann_version强相关。cann-recipes-infer内置了一个workspace_size_table.json根据(device_type, cann_version)查表获取推荐值。例如atlas200dkCANN 6.3的推荐值是134217728128MB。设置过小会导致aclrtCreateContext()失败过大则无益因为运行时会按需申请。log_level这个字段控制aclrtSetLogAutoPrintFlag()的行为。cann-recipes-infer将log_level: ERROR映射为ACL_LOG_ERRORINFO映射为ACL_LOG_INFO。但有一个隐藏技巧当log_level: DEBUG时Config类会额外调用aclrtSetLogPrintLevel(ACL_LOG_DEBUG)并启用aclrtSetLogAutoPrintFlag(1)同时设置环境变量ASCEND_GLOBAL_LOG_LEVEL3。这会让 AscendCL 输出详细的算子执行时间、内存分配 trace对性能调优至关重要。但要注意DEBUG日志会产生海量输出仅建议在单次调试时开启。3.3preprocess与postprocess模块数据流水线的“编排脚本”这两个模块定义了数据如何从原始图像进入模型又如何从模型输出变成业务可用的结果。它们不是简单的函数调用而是可插拔的数据处理流水线。preprocess的pipeline字段这是一个有序列表定义了预处理步骤的执行顺序。每个步骤是一个字典包含name如resize,normalize和params参数。cann-recipes-infer的Preprocessor类会按顺序实例化每个步骤的处理器如ResizeProcessor,NormalizeProcessor并构建一个Pipeline对象。关键在于Pipeline的__call__()方法不是简单地串行调用而是惰性求值Lazy Evaluation它只在Pipeline.process(image)被调用时才真正执行所有步骤并且会缓存中间结果。例如resize步骤的输出resized_image会被normalize步骤直接复用避免重复内存拷贝。params中的interpolation参数如bilinear会被映射到 OpenCV 的cv2.INTER_LINEAR而cv2.resize()的调用是在Pipeline内部完成的与aclrt无关确保 CPU 预处理的高效性。postprocess的type字段这决定了后处理的算法实现。cann-recipes-infer支持yolo,ssd,classification三种类型。type: yolo时Postprocessor会加载yolo_postprocess.py其中decode_boxes()函数会根据model.input_shape和model.output_shapes计算 anchor box 的缩放比例并应用sigmoid激活和nms非极大值抑制。nms_threshold和score_threshold参数直接来自 YAML 的postprocess.params。这里有个性能陷阱nms是 CPU 操作如果score_threshold设得太低如0.001会导致nms输入的候选框数量暴增YOLOv5 输出可能有 25350 个框nms时间从几毫秒飙升到几百毫秒。cann-recipes-infer在Config初始化时会检查score_threshold是否在[0.01, 0.9]合理范围内超出则警告WARNING: score_threshold 0.001 is too low, may cause slow NMS。preprocess与postprocess的dtype字段这指定了数据在 CPU 和 NPU 之间传输时的精度。preprocess.dtype: float32表示cv2.resize()和cv2.normalize()输出的 numpy array 是np.float32aclrtMemcpyHtoDAsync()会将其作为 FP32 数据传入 NPUpostprocess.dtype: float16表示aclrtMemcpyDtoHAsync()读取的输出数据是 FP16Postprocessor会先astype(np.float32)再进行nms。这个dtype必须与runtime.precision_mode匹配如果precision_mode: force_fp16则preprocess.dtype应为float16否则aclrtMemcpyHtoDAsync()会因数据类型不匹配而失败。cann-recipes-infer的Config类会在validate()阶段进行这个交叉校验。4. 实操过程从 YAML 文件到aclrtCreateContext()的完整链路4.1 启动入口infer.py如何触发配置加载整个流程始于infer.py的main()函数。我们跟随代码看 YAML 如何一步步“活”起来# infer.py if __name__ __main__: # 第一步解析命令行参数 args parse_args() # 获取 --config config/yolov5s.yaml --model_path models/yolov5s.om 等 # 第二步加载配置 config Config.from_file(args.config) # 这是核心 # 第三步应用命令行覆盖 config.override_from_args(args) # 例如 --device_id 1 会覆盖 config.hardware.device_id # 第四步验证配置 config.validate() # 执行 Schema 校验、路径检查、交叉校验 # 第五步初始化 AscendCL 运行时 acl.init() # 初始化 ACL 运行时 acl.rt.set_device(config.runtime.device_id) # 设置设备 context acl.rt.create_context(config.runtime.device_id) # 创建上下文 # ... 后续模型加载、推理等Config.from_file()是整个链条的起点。它的内部流程如下load_yaml_file()调用ruamel.yaml.YAML(typsafe, pureTrue).load()加载args.config文件返回一个CommentedMap。pureTrue确保使用纯 Python 解析器避免 C 扩展的潜在安全风险typsafe禁用所有危险标签。resolve_includes()递归解析!include标签。例如如果yolov5s.yaml包含hardware: !include hardware/atlas200dk.yamlresolve_includes()会读取hardware/atlas200dk.yaml将其内容合并到主配置中。这个过程是深度合并deep merge不是浅层覆盖。apply_env_substitution()解析!env标签。例如model_path: !env MODEL_DIR /home/ascend/models会读取环境变量MODEL_DIR如果存在则替换否则使用默认值/home/ascend/models。create_config_object()将合并后的CommentedMap传递给Config类的__init__()。Config.__init__()会调用self._validate_schema()加载schemas/model_schema.json并执行jsonschema.validate()。调用self._complete_dynamic_fields()执行device_id自动补全、路径绝对化。调用self._bind_to_runtime()将runtime字段映射到acl::AclConfig结构体。这个from_file()流程耗时约 50-200ms取决于 YAML 大小和!include层数但它把所有潜在错误都拦截在了acl.init()之前。相比传统方式先acl.init()再aclrtSetDevice()再aclrtLoadModel()最后在aclrtExecute()时才发现input_shape错了这种前置校验将平均调试时间从 15 分钟缩短到 2 分钟。4.2 关键环节实现Config类的to_acl_config()方法详解Config.to_acl_config()是 YAML 配置与 AscendCL 运行时 API 的最后一道桥梁。我们来看它的核心实现简化版# core/config.py def to_acl_config(self): Convert Config instance to acl::AclConfig structure acl_config acl.AclConfig() # 1. Precision mode mapping precision_map { allow_fp32_to_fp16: acl.ACL_FP32_TO_FP16, force_fp16: acl.ACL_FORCE_FP16, must_keep_origin_dtype: acl.ACL_MUST_KEEP_ORIGIN_DTYPE } if self.runtime.precision_mode not in precision_map: raise ConfigError(fInvalid precision_mode: {self.runtime.precision_mode}) acl_config.precisionMode precision_map[self.runtime.precision_mode] # 2. Memory configuration acl_config.bufferPoolSize self.runtime.memory.buffer_pool_size acl_config.workspaceSize self.runtime.memory.workspace_size # 3. Log level mapping log_level_map {ERROR: 1, WARNING: 2, INFO: 3, DEBUG: 4} acl_config.logLevel log_level_map.get(self.runtime.log_level, 3) # 4. Device ID (for multi-device context) acl_config.deviceId self.runtime.device_id # 5. Custom options (e.g., for profiling) if self.runtime.profiling: acl_config.profilingMode acl.ACL_PROFILING_MODE_ENABLE acl_config.profilingOptions training,task return acl_config这个方法的关键在于零拷贝绑定。acl.AclConfig()是一个 ctypes 结构体acl_config.precisionMode等字段直接对应 C 结构体的内存偏移。当acl.rt.create_context()被调用时它接收的就是这个acl_config对象的地址无需任何序列化/反序列化。这意味着 YAML 中的precision_mode: force_fp16在内存中就是0x02ACL_FORCE_FP16的整数值aclrtCreateContext()拿到的就是这个原始值。这种设计消除了中间转换层保证了配置传递的极致效率和确定性。4.3 实操现场记录一次典型的配置错误排查上周一个客户报告cann-recipes-infer在 Atlas 300I 上运行 YOLOv10 时aclrtCreateContext()返回ACL_ERROR_INVALID_PARAMETER。我们按标准流程排查检查 YAML 语法ruamel.yaml加载无报错排除语法错误。检查 Schema 校验Config.validate()通过说明字段名、类型、必填项都 OK。检查路径model_path存在aclGetModelDesc()成功读取元数据。检查device_idnpu-smi info显示device_id: 0可用。检查precision_modeyolov10.yaml中是force_fp16CANN 6.3 支持。卡住了。我们启用了log_level: DEBUG重新运行日志中出现一行[ACL] ERROR: Invalid workspace size 1073741824, max allowed is 536870912原来workspace_size设置为10737418241GB但 Atlas 300I 的最大 workspace 是536870912512MB。问题出在yolov10.yaml的runtime.memory.workspace_size字段。我们查workspace_size_table.json发现 Atlas 300I CANN 6.3 的推荐值是536870912而客户 copy-paste 了 Atlas 200DK 的配置后者推荐134217728但客户误写成了1073741824。修复方法很简单将workspace_size改为536870912。这个案例凸显了Config类的价值它没有在validate()阶段就报错因为workspace_size的 Schema 只校验了“是正整数”没校验“是否超过硬件上限”。真正的校验发生在aclrtCreateContext()的 C 层而DEBUG日志给出了精准的错误原因。cann-recipes-infer的设计哲学是Schema 校验保证配置的“形式正确”运行时 API 校验保证配置的“语义正确”。两者缺一不可。5. 常见问题与排查技巧实录5.1 YAML 解析阶段常见问题速查表问题现象根本原因排查技巧解决方案yaml.scanner.ScannerError: while scanning a simple keyYAML 缩进不一致或使用了 tab 而非空格用cat -A config.yaml查看隐藏字符确认所有缩进是 2 或 4 个空格用 VS Code 的 YAML 插件开启“Detect Indentation”和“Insert Spaces”ConfigError: device_id is a required propertyhardware部分缺失device_id字段且未设置环境变量ATLAS_DEVICE检查config.yaml中hardware:下是否有device_id:行运行echo $ATLAS_DEVICE在hardware:下添加device_id: 0或 export AT
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。