当前位置: 首页 > news >正文

Flink源码解析之:如何根据算法生成StreamGraph过程

Flink源码解析之:如何根据算法生成StreamGraph过程

在我们日常编写Flink应用的时候,会首先创建一个StreamExecutionEnvironment.getExecutionEnvironment()对象,在添加一些自定义处理算子后,会调用env.execute来执行定义好的Flink应用程序。我们知道,Flink在实际执行任务前,会根据应用生成StreamGraph,再生成JobGraph,最终提交到集群中进行执行。那么Flink是如何将我们自定义的应用程序转换成StreamGraph的呢?这一过程中实现了什么逻辑? 接下来,我们通过源码来深入了解一下。

在本次分析源码的过程中,主要涉及到StreamExecutionEnvironmentDataStreamTransformationStreamGraphStreamGraphGenerator几下个类,这里先汇总介绍一下在生成StreamGraph过程中,这些类的交互处理流程,有了这个印象后,再阅读下面的源码流程,更容易串起来和理解。

在这里插入图片描述

一、Function -> Transformation转换

在我们编写Flink应用程序时,会自定义一系列算子拼接在数据流链路中,比如,当我们调用datastream.flatMap(flatMapFunction)方法时,就会将传入的算子函数,转换成Transformation对象,添加到StreamExecutionEnvironment对象的List<Transformation<?>> transformations 属性中。接下来,我们就来看一下是如何进行转换的。

首先进入到DataStream类中,找到比如flatMap方法:

public <R> SingleOutputStreamOperator<R> flatMap(FlatMapFunction<T, R> flatMapper) {TypeInformation<R> outType =TypeExtractor.getFlatMapReturnTypes(clean(flatMapper), getType(), Utils.getCallLocationName(), true);return flatMap(flatMapper, outType);
}public <R> SingleOutputStreamOperator<R> flatMap(FlatMapFunction<T, R> flatMapper, TypeInformation<R> outputType) {return transform("Flat Map", outputType, new StreamFlatMap<>(clean(flatMapper)));
}

上面代码中,将flatMap封装到StreamFlatMap方法中,用于表示一个StreamOperator操作符。StreamFlatMap操作符会针对每一个StreamRecord,通过processElement方法调用用户函数去处理该流数据:

@Override
public void processElement(StreamRecord<IN> element) throws Exception {collector.setTimestamp(element);// 调用用户函数执行数据流元素处理逻辑userFunction.flatMap(element.getValue(), collector);
}

回到DataSteramFlatMap方法中,我们继续看transform方法里做了什么:

@PublicEvolving
public <R> SingleOutputStreamOperator<R> transform(String operatorName,TypeInformation<R> outTypeInfo,OneInputStreamOperator<T, R> operator) {return doTransform(operatorName, outTypeInfo, SimpleOperatorFactory.of(operator));
}

上面根据传入的StreamOperator创建一个SimpleOperatorFactory对象,StreamOperatorFactory是一个工厂类,其主职责是为特定类型的StreamOperator在运行时创建实例。它还提供了其他附加功能如做一些操作配置,比如chaining。

接下来继续进入doTransform方法:

protected <R> SingleOutputStreamOperator<R> doTransform(String operatorName,TypeInformation<R> outTypeInfo,StreamOperatorFactory<R> operatorFactory) {// read the output type of the input Transform to coax out errors about MissingTypeInfo// 获取当前数据流(上一个Transformation)的输出类型。这样可以做类型检查,并在类型信息缺失时提前引发错误。transformation.getOutputType();// 创建一个新的OneInputTransformation,这个新的OneInputTransformation即为要添加的新操作// 对于flatMap操作来说,不存在分区,所以上下游是一对一的关系,所以这里用的是OneInputTransformationOneInputTransformation<T, R> resultTransform =new OneInputTransformation<>(this.transformation,operatorName,operatorFactory,outTypeInfo,environment.getParallelism());// 创建一个SingleOutputStreamOperator对象,该对象将接收新加入的操作的输出@SuppressWarnings({"unchecked", "rawtypes"})SingleOutputStreamOperator<R> returnStream =new SingleOutputStreamOperator(environment, resultTransform);// 将新的Transformation添加到当前的执行环境中,这个操作将并入到计算流图中。getExecutionEnvironment().addOperator(resultTransform);// 代表了新添加的操作输出结果的数据流,便于在这个数据流上继续构建后续的计算。return returnStream;
}

上述代码内容就是将userFunction转换成Transformation的具体执行逻辑了,因为我们最初举例是flatMap方法,因此在将userFunction转换成Transformation时,会使用OneInputTransformation来表示。同时这里可以看到,在转换完成后,会调用getExecutionEnvironment().addOperator(resultTransform)将得到的Transformation添加到当前执行环境的计算流图中,实际上也就是添加到我们刚刚所说的执行环境的List<Transformation<?>> transformations属性中了。

二、StreamGraphGenerator生成StreamGraph

在将用户函数userFunction转换成Transformation并保存到StreamExecutionEnvironment的transformations属性中后,我们就收集抽象好了所有的用户函数及处理链路,接下来,就是根据这些封装好的Transformation来生成StreamGraph。

首先进入到StreamExecutionEnvironmentexecute执行入口方法中:

public JobExecutionResult execute() throws Exception {return execute(getStreamGraph());
}@Internal
public StreamGraph getStreamGraph() {return getStreamGraph(true);
}@Internal
public StreamGraph getStreamGraph(boolean clearTransformations) {final StreamGraph streamGraph = getStreamGraphGenerator(transformations).generate();if (clearTransformations) {transformations.clear();}return streamGraph;
}

在上面的getStreamGraph方法中,使用getStreamGraphGenerator方法生成一个StreamGraphGenerator对象,这里的transformations参数,实际上指的就是上面保存的每个用户函数转换得到的Transformation对象。

接下来,我们主要看generator方法,进入到StreamGraphGenerator类中,这个类也是创建StreamGraph最核心的类。

public StreamGraph generate() {// 根据不同的配置信息创建一个StreamGraph对象streamGraph = new StreamGraph(executionConfig, checkpointConfig, savepointRestoreSettings);// 设置 StreamGraph 是否在任务结束后启用checkpoint,这个布尔值从配置中获取。streamGraph.setEnableCheckpointsAfterTasksFinish(configuration.get(ExecutionCheckpointingOptions.ENABLE_CHECKPOINTS_AFTER_TASKS_FINISH));shouldExecuteInBatchMode = shouldExecuteInBatchMode();configureStreamGraph(streamGraph);// 	初始化一个哈希映射alreadyTransformed,用于存储已经被转换过的Transformation。alreadyTransformed = new HashMap<>();// 遍历transformations列表,对每个transformation对象进行转换// 这里是转换的核心逻辑for (Transformation<?> transformation : transformations) {transform(transformation);}// 将slotSharingGroupResources设置为StreamGraph的资源配置。streamGraph.setSlotSharingGroupResource(slotSharingGroupResources);setFineGrainedGlobalStreamExchangeMode(streamGraph);// 获取StreamGraph中所有的StreamNode,检查它们的输入边缘是否满足禁用未对齐的checkpointing的条件,如果满足条件,则将边的supportsUnalignedCheckpoints属性设置为false。for (StreamNode node : streamGraph.getStreamNodes()) {if (node.getInEdges().stream().anyMatch(this::shouldDisableUnalignedCheckpointing)) {for (StreamEdge edge : node.getInEdges()) {edge.setSupportsUnalignedCheckpoints(false);}}}// 清理streamGraph和alreadyTransformed以释放资源,并防止后续的错误使用,并保存当前的streamGraph实例到builtStreamGraph中。final StreamGraph builtStreamGraph = streamGraph;alreadyTransformed.clear();alreadyTransformed = null;streamGraph = null;// 最后返回构建好的StreamGraph。return builtStreamGraph;
}

上面代码中,最主要的核心逻辑在for循环遍历transformations中,调用transform方法对每个Transformation对象进行转换。我们主要进入到该方法中进行分析:

/*** Transforms one {@code Transformation}.** <p>This checks whether we already transformed it and exits early in that case. If not it* delegates to one of the transformation specific methods.*/
private Collection<Integer> transform(Transformation<?> transform) {// 快速检查传入的 transform 对象是否已经在 alreadyTransformed 字典(一个缓存)中,如果已存在则直接返回对应的ID,这种早期退出的机制避免了对同一任务的重复转换。if (alreadyTransformed.containsKey(transform)) {return alreadyTransformed.get(transform);}LOG.debug("Transforming " + transform);if (transform.getMaxParallelism() <= 0) {// if the max parallelism hasn't been set, then first use the job wide max parallelism// from the ExecutionConfig.int globalMaxParallelismFromConfig = executionConfig.getMaxParallelism();if (globalMaxParallelismFromConfig > 0) {transform.setMaxParallelism(globalMaxParallelismFromConfig);}}//  若 transform 对象指定了 SlotSharingGroup ,那么会从 SlotSharingGroup 中提取资源并更新到 slotSharingGroupResources 中。transform.getSlotSharingGroup().ifPresent(slotSharingGroup -> {final ResourceSpec resourceSpec =SlotSharingGroupUtils.extractResourceSpec(slotSharingGroup);if (!resourceSpec.equals(ResourceSpec.UNKNOWN)) {slotSharingGroupResources.compute(slotSharingGroup.getName(),(name, profile) -> {if (profile == null) {return ResourceProfile.fromResourceSpec(resourceSpec, MemorySize.ZERO);} else if (!ResourceProfile.fromResourceSpec(resourceSpec, MemorySize.ZERO).equals(profile)) {throw new IllegalArgumentException("The slot sharing group "+ slotSharingGroup.getName()+ " has been configured with two different resource spec.");} else {return profile;}});}});// call at least once to trigger exceptions about MissingTypeInfo// 调用 transform.getOutputType() 进行安全检查,确保类型信息的完整性。transform.getOutputType();// 根据 transform 对象的类型获取对应的转换逻辑 translator. @SuppressWarnings("unchecked")final TransformationTranslator<?, Transformation<?>> translator =(TransformationTranslator<?, Transformation<?>>)translatorMap.get(transform.getClass());// 如果找到了相应的 translator,使用它进行转换;否则,使用旧的转换策略 legacyTransform()。Collection<Integer> transformedIds;if (translator != null) {transformedIds = translate(translator, transform);} else {transformedIds = legacyTransform(transform);}// 在转换完成后,检查 transform 是否已经被记录在 alreadyTransformed 字典中。如果尚未记录,则将转换后的对象ID添加到字典中。// need this check because the iterate transformation adds itself before// transforming the feedback edgesif (!alreadyTransformed.containsKey(transform)) {alreadyTransformed.put(transform, transformedIds);}// 将转换后产生的节点ID返回以供后续使用。return transformedIds;
}

很明显,这种转换不可能多次进行,因为这会浪费计算资源。因此,我们需要一个机制来记录哪些Transformation已经被转换过。在Flink中,这是通过一个名为alreadyTransformed的哈希映射实现的。如果当前的Transformation已经存在于alreadyTransformed中,那么就无需再次进行转换,直接返回对应的集合即可。

接下来,根据transform的具体类型,从translatorMap中获取相应的translator转换器(具体的translatorMap内容可以在代码中看到)。找到转换器后,调用translate方法来执行转换。那么我们又需要进入到translate方法中一探究竟:

private Collection<Integer> translate(final TransformationTranslator<?, Transformation<?>> translator,final Transformation<?> transform) {checkNotNull(translator);checkNotNull(transform);// 通过调用getParentInputIds()方法获取当前transform对象的所有输入(父级Transformation)的ID。final List<Collection<Integer>> allInputIds = getParentInputIds(transform.getInputs());// 再次检查当前transform对象是否已在alreadyTransformed字典中,如果是,直接返回对应的ID。// the recursive call might have already transformed thisif (alreadyTransformed.containsKey(transform)) {return alreadyTransformed.get(transform);}// 确定slotSharingGroup,这是一个根据transform输入和slotSharingGroup名称,决定slot sharing策略的过程。final String slotSharingGroup =determineSlotSharingGroup(transform.getSlotSharingGroup().isPresent()? transform.getSlotSharingGroup().get().getName(): null,allInputIds.stream().flatMap(Collection::stream).collect(Collectors.toList()));// 创建一个TransformationTranslator.Context对象,里面包含了StreamGraph,slotSharingGroup和配置信息,该上下文会在转换过程中使用。final TransformationTranslator.Context context =new ContextImpl(this, streamGraph, slotSharingGroup, configuration);// 根据执行模式不同,调用转换方法translateForBatch()或translateForStreaming()进行具体的转换工作。return shouldExecuteInBatchMode? translator.translateForBatch(transform, context): translator.translateForStreaming(transform, context);
}

每一个TransformationTranslator实例都绑定了一个特定类型的Transformation的转换逻辑,例如OneInputTransformationTranslator,SourceTransformation等。通过这份代码,我们可以看到Flink的灵活性和可扩展性。你可以为特定的Transformation添加不同的激活逻辑或者处理逻辑。这种设计确保了Flink在处理不同类型Transformation时的高效性,并且很容易添加新类型的Transformation。

这里,我们仍然以OneInputTransformationTranslator的转换逻辑来举例,看一下Flink的Transformation转换逻辑执行了什么操作?

protected Collection<Integer> translateInternal(final Transformation<OUT> transformation,final StreamOperatorFactory<OUT> operatorFactory,final TypeInformation<IN> inputType,@Nullable final KeySelector<IN, ?> stateKeySelector,@Nullable final TypeInformation<?> stateKeyType,final Context context) {checkNotNull(transformation);checkNotNull(operatorFactory);checkNotNull(inputType);checkNotNull(context);// 即获取 StreamGraph、slotSharingGroup 和transformation的 ID。final StreamGraph streamGraph = context.getStreamGraph();final String slotSharingGroup = context.getSlotSharingGroup();final int transformationId = transformation.getId();final ExecutionConfig executionConfig = streamGraph.getExecutionConfig();// addOperator() 方法把转换Transformation 添加到 StreamGraph 中。此操作包括transformation的 ID,slotSharingGroup,CoLocationGroupKey,工厂类,输入类型,输出类型以及操作名。streamGraph.addOperator(transformationId,slotSharingGroup,transformation.getCoLocationGroupKey(),operatorFactory,inputType,transformation.getOutputType(),transformation.getName());// 如果 stateKeySelector(用于从输入中提取键的函数)非空,使用 stateKeyType 创建密钥序列化器,并在 StreamGraph 中设置用于接收单输入的状态键if (stateKeySelector != null) {TypeSerializer<?> keySerializer = stateKeyType.createSerializer(executionConfig);streamGraph.setOneInputStateKey(transformationId, stateKeySelector, keySerializer);}// 根据 Transformation 和 executionConfig 设置并行度。int parallelism =transformation.getParallelism() != ExecutionConfig.PARALLELISM_DEFAULT? transformation.getParallelism(): executionConfig.getParallelism();streamGraph.setParallelism(transformationId, parallelism);streamGraph.setMaxParallelism(transformationId, transformation.getMaxParallelism());final List<Transformation<?>> parentTransformations = transformation.getInputs();checkState(parentTransformations.size() == 1,"Expected exactly one input transformation but found "+ parentTransformations.size());// 根据转换的输入和输出添加边到 StreamGraph。每个输入转换都添加一条边。for (Integer inputId : context.getStreamNodeIds(parentTransformations.get(0))) {streamGraph.addEdge(inputId, transformationId, 0);}// 方法返回包含转换 ID 的单个元素集合。return Collections.singleton(transformationId);
}

上面这段代码,实际上就是构建StreamGraph的主体逻辑部分了,translateInternal() 方法实现了从 Transformation 到 StreamGraph 中操作的转换。在该方法中,对于每一个Transformation,会调用streamGraph.addOperator方法,生成一个StreamNode对象,存储在StreamGraph的streamNode属性中,该属性是一个Map<Integer, StreamNode>结构,表示每个Transformation ID对应的StreamNode节点。

protected StreamNode addNode(Integer vertexID,@Nullable String slotSharingGroup,@Nullable String coLocationGroup,Class<? extends TaskInvokable> vertexClass,StreamOperatorFactory<?> operatorFactory,String operatorName) {if (streamNodes.containsKey(vertexID)) {throw new RuntimeException("Duplicate vertexID " + vertexID);}StreamNode vertex =new StreamNode(vertexID,slotSharingGroup,coLocationGroup,operatorFactory,operatorName,vertexClass);streamNodes.put(vertexID, vertex);return vertex;
}

看完translateInternal方法中streamGraph.addOperator的执行逻辑后,接下来还需要关注的一个步骤是streamGraph.addEdge,这里是连接StreamGraph中各StreamNode节点的逻辑所在:

public void addEdge(Integer upStreamVertexID, Integer downStreamVertexID, int typeNumber) {addEdgeInternal(upStreamVertexID,downStreamVertexID,typeNumber,null,new ArrayList<String>(),null,null);
}

addEdgeInternal方法中,会区分当前节点是虚拟节点还是物理节点,从而添加物理边还是虚拟边。由于我们用OneInputTransformationTranslator会创建物理节点,所以进入到创建物理边的分支代码中:

private void createActualEdge(Integer upStreamVertexID,Integer downStreamVertexID,int typeNumber,StreamPartitioner<?> partitioner,OutputTag outputTag,StreamExchangeMode exchangeMode) {// 首先通过节点ID获取上游和下游的StreamNode。StreamNode upstreamNode = getStreamNode(upStreamVertexID);StreamNode downstreamNode = getStreamNode(downStreamVertexID);// 检查分区器partitioner是否已经设置,如果没有设置,且上游节点与下游节点的并行度相等,那么使用ForwardPartitioner; 如果并行度不相等,则使用RebalancePartitioner。// If no partitioner was specified and the parallelism of upstream and downstream// operator matches use forward partitioning, use rebalance otherwise.if (partitioner == null&& upstreamNode.getParallelism() == downstreamNode.getParallelism()) {partitioner = new ForwardPartitioner<Object>();} else if (partitioner == null) {partitioner = new RebalancePartitioner<Object>();}if (partitioner instanceof ForwardPartitioner) {if (upstreamNode.getParallelism() != downstreamNode.getParallelism()) {throw new UnsupportedOperationException("Forward partitioning does not allow "+ "change of parallelism. Upstream operation: "+ upstreamNode+ " parallelism: "+ upstreamNode.getParallelism()+ ", downstream operation: "+ downstreamNode+ " parallelism: "+ downstreamNode.getParallelism()+ " You must use another partitioning strategy, such as broadcast, rebalance, shuffle or global.");}}if (exchangeMode == null) {exchangeMode = StreamExchangeMode.UNDEFINED;}/*** Just make sure that {@link StreamEdge} connecting same nodes (for example as a result of* self unioning a {@link DataStream}) are distinct and unique. Otherwise it would be* difficult on the {@link StreamTask} to assign {@link RecordWriter}s to correct {@link* StreamEdge}.*/// 在上述配置都设置好之后,创建StreamEdge对象,并将其添加到上游节点的出边和下游节点的入边。int uniqueId = getStreamEdges(upstreamNode.getId(), downstreamNode.getId()).size();StreamEdge edge =new StreamEdge(upstreamNode,downstreamNode,typeNumber,partitioner,outputTag,exchangeMode,uniqueId);getStreamNode(edge.getSourceId()).addOutEdge(edge);getStreamNode(edge.getTargetId()).addInEdge(edge);
}

从上述代码中可以看出,createActualEdge()方法实现了在StreamGraph中添加实际的边的过程,这是构建Flink StreamGraph的一个重要步骤。

至此,我们就看到了创建StreamGraph,并根据Transformation来生成StreamNode,并添加StreamEdge边的过程,最终构建好一个完成的StreamGraph来表示Flink应用程序的数据流执行拓扑图。

当然这里我们只是以OneInputTransformationTranslator转换器举例来分析流程,实际上其他的转换器应该会更复杂一些,有兴趣的可以继续深入研究,本文便不再赘述。同时,本文也仍然有很多细节暂时因为理解不够深入没有涉及,欢迎各位一起交流学习。

最终,在我们构造好StreamGraph后,就需要考虑如何将StreamGraph转换成JobGraph了,下一篇,将继续介绍StreamGraph -> JobGraph的转换。

相关文章:

Flink源码解析之:如何根据算法生成StreamGraph过程

Flink源码解析之&#xff1a;如何根据算法生成StreamGraph过程 在我们日常编写Flink应用的时候&#xff0c;会首先创建一个StreamExecutionEnvironment.getExecutionEnvironment()对象&#xff0c;在添加一些自定义处理算子后&#xff0c;会调用env.execute来执行定义好的Flin…...

矩阵简单问题(Java)

问题&#xff1a; 顺时针打印二维方阵&#xff1a; 1 2 3 4 15 5 6 7 8 14 9 10 11 12 13 13 14 15 16 public class Test1 {public static void main(String[] args) {int[][] arr new int[][]{{1, 2, 3, 4,100},{5, 6, 7, 8,101},{9, 10, 11, 12,102},{13, 14, 15, 16,…...

Elasticsearch DSL版

文章目录 1.索引库操作创建索引库&#xff1a;删除索引库&#xff1a;查询索引库&#xff1a;修改索引库&#xff1a;总结 2.文档操作创建文档&#xff1a;查询文档&#xff1a;删除文档&#xff1a;全量修改文档&#xff1a;增量修改文档&#xff1a;总结 3.DSL查询语法&#…...

2024-12-29-sklearn学习(26)模型选择与评估-交叉验证:评估估算器的表现 今夜偏知春气暖,虫声新透绿窗纱。

文章目录 sklearn学习(26) 模型选择与评估-交叉验证&#xff1a;评估估算器的表现26.1 计算交叉验证的指标26.1.1 cross_validate 函数和多度量评估26.1.2 通过交叉验证获取预测 26.2 交叉验证迭代器26.2.1 交叉验证迭代器–循环遍历数据26.2.1.1 K 折26.2.1.2 重复 K-折交叉验…...

STM32CUBEIDE FreeRTOS操作教程(十二):std dynamic memory 标准动态内存

STM32CUBEIDE FreeRTOS操作教程&#xff08;十二&#xff09;&#xff1a;std dynamic memory 标准动态内存 STM32CUBE开发环境集成了STM32 HAL库进行FreeRTOS配置和开发的组件&#xff0c;不需要用户自己进行FreeRTOS的移植。这里介绍最简化的用户操作类应用教程。以STM32F40…...

异步爬虫之aiohttp的使用

在上一篇博客我们介绍了异步爬虫的基本原理和 asyncio 的基本用法&#xff0c;并且在最后简单提及了使用aiohttp 实现网页爬取的过程。本篇博客我们介绍一下 aiohttp 的常见用法。 基本介绍 前面介绍的 asyncio模块&#xff0c;其内部实现了对 TCP、UDP、SSL协议的异步操作&a…...

【Rust自学】9.1. 不可恢复的错误以及panic!

喜欢的话别忘了点赞、收藏加关注哦&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 9.1.1. Rust错误处理概述 Rust拥有极高的可靠性&#xff0c;这也延伸到了错误处理的领域。比如说在大部分情况下&#xff0c;Rust会迫使你…...

【老张的程序人生】一天时间,我成软考高级系统分析师

今年下半年&#xff0c;我心血来潮报考了软考高级系统分析师。彼时的我&#xff0c;工作繁忙至极&#xff0c;一周十四节课&#xff0c;班主任的职责压身&#xff0c;还兼任教学管理事务&#xff0c;每日忙得晕头转向&#xff0c;那点可怜的闲暇时光&#xff0c;也都奉献给了游…...

vue使用el-select下拉框自定义复选框

在 Vue 开发中&#xff0c;高效且美观的组件能极大地提升用户体验和开发效率。在vue中使用elementplus 的 el-select下拉框实现了一个自定义的多选下拉框组件。 一、代码功能概述 这段代码创建了一个可多选的下拉框组件&#xff0c;通过el-select和el-checkbox-group结合的方…...

k8s基础(2)—Kubernetes-Namespace

一、Namespace概述 名字空间 在 Kubernetes 中&#xff0c;名字空间&#xff08;Namespace&#xff09; 提供一种机制&#xff0c;将同一集群中的资源划分为相互隔离的组。 同一名字空间内的资源名称要唯一&#xff0c;但跨名字空间时没有这个要求。 名字空间作用域仅针对带有…...

APM for Large Language Models

APM for Large Language Models 随着大语言模型&#xff08;LLMs&#xff09;在生产环境中的广泛应用&#xff0c;确保其可靠性和可观察性变得至关重要。应用性能监控&#xff08;APM&#xff09;在这一过程中发挥了关键作用&#xff0c;帮助开发者和运维人员深入了解LLM系统的…...

Spark Runtime Filter

Runtime Filter 参考链接&#xff1a; https://docs.google.com/document/d/16IEuyLeQlubQkH8YuVuXWKo2-grVIoDJqQpHZrE7q04/edit?tabt.0https://www.modb.pro/db/557718https://issues.apache.org/jira/browse/SPARK-32268https://github.com/apache/spark/pull/35789https…...

AI大模型系列之七:Transformer架构讲解

目录 Transformer网络是什么&#xff1f; 输入模块结构&#xff1a; 编码器模块结构&#xff1a; 解码器模块: 输出模块结构&#xff1a; Transformer 具体是如何工作的&#xff1f; Transformer核心思想是什么&#xff1f; Transformer的代码架构 自注意力机制是什么…...

基于51单片机(STC12C5A60S2)和8X8彩色点阵屏(WS2812B驱动)的小游戏《贪吃蛇》(普中开发板矩阵按键控制)

目录 系列文章目录前言一、效果展示二、原理分析三、各模块代码1、定时器02、矩阵按键3、8X8彩色点阵屏 四、主函数总结 系列文章目录 前言 《贪吃蛇》&#xff0c;一款经典的、怀旧的小游戏&#xff0c;单片机入门必写程序。 以《贪吃蛇》为载体&#xff0c;熟悉各种屏幕的使…...

遇到复杂的 递归查询sql 需要oracle 转pgsql 可以把数据表结构给ai

遇到复杂的 递归查询sql 需要oracle 转pgsql 可以把数据表结构给ai 并且 建立备份表 把需要的很少的数据放到表里面 这样 ai 可以很好的判断sql 咋写 还可以&#xff0c;让ai解释oracle sql 然后拿到描述和表和字段&#xff0c;给ai让他生成pgsql 的sql&#xff0c;亲测有效...

Zynq PS端外设之GPIO

1. GPIO&#xff08;通用输入/输出&#xff09; GPIO外设有4个Bank&#xff0c;Bank0/1通过MIO连接到PS的引脚上&#xff1b;Bank2/3通过EMIO连接到PL的引脚上。 注意&#xff1a;Bank1的电平要改成LVCOMS 1.8 GPIO寄存器 寄存器&#xff1a; DATA_RO&#xff1a; 读取GPIO的输…...

Spring Boot项目开发常见问题及解决方案(上)

启动相关问题 问题 1&#xff1a;项目启动时报错“找不到主类” 在使用 Spring Boot 打包成可执行 JAR 文件后启动&#xff0c;有时会遇到这个头疼的问题。通常是因为打包配置有误或者项目结构不符合要求。 解决方案&#xff1a; 首先&#xff0c;检查 pom.xml&#xff08;Ma…...

Elasticsearch: 高级搜索

这里写目录标题 一、match_all匹配所有文档1、介绍&#xff1a; 二、精确匹配1、term单字段精确匹配查询2、terms多字段精确匹配3、range范围查询4、exists是否存在查询5、ids根据一组id查询6、prefix前缀匹配7、wildcard通配符匹配8、fuzzy支持编辑距离的模糊查询9、regexp正则…...

STM32 拓展 电源控制

目录 电源控制 电源框图 VDDA供电区域 VDD供电区域 1.8V低电压区域 后备供电区域 电压调节器 上电复位和掉电复位 可编程电压检测器(PVD) 低功耗 睡眠模式(只有CUP(老板)睡眠) 进入睡眠模式 退出睡眠模式 停机(停止)模式(只留核心区域(上班)) 进入停…...

SpringBootWeb案例-1

文章目录 SpringBootWeb案例1. 准备工作1.1 需求&环境搭建1.1.1 需求说明1.1.2 环境搭建 1.2 开发规范 2. 部门管理2.1 查询部门2.1.1 原型和需求2.1.2 接口文档2.1.3 思路分析2.1.4 功能开发2.1.5 功能测试 2.2 前后端联调2.3 删除部门2.3.1 需求2.3.2 接口文档2.3.3 思路…...

【Java学习笔记】Arrays类

Arrays 类 1. 导入包&#xff1a;import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序&#xff08;自然排序和定制排序&#xff09;Arrays.binarySearch()通过二分搜索法进行查找&#xff08;前提&#xff1a;数组是…...

Java如何权衡是使用无序的数组还是有序的数组

在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止

<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet&#xff1a; https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...

学校招生小程序源码介绍

基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码&#xff0c;专为学校招生场景量身打造&#xff0c;功能实用且操作便捷。 从技术架构来看&#xff0c;ThinkPHP提供稳定可靠的后台服务&#xff0c;FastAdmin加速开发流程&#xff0c;UniApp则保障小程序在多端有良好的兼…...

Caliper 配置文件解析:config.yaml

Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...

DingDing机器人群消息推送

文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人&#xff0c;点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置&#xff0c;详见说明文档 成功后&#xff0c;记录Webhook 2 API文档说明 点击设置说明 查看自…...

HybridVLA——让单一LLM同时具备扩散和自回归动作预测能力:训练时既扩散也回归,但推理时则扩散

前言 如上一篇文章《dexcap升级版之DexWild》中的前言部分所说&#xff0c;在叠衣服的过程中&#xff0c;我会带着团队对比各种模型、方法、策略&#xff0c;毕竟针对各个场景始终寻找更优的解决方案&#xff0c;是我个人和我司「七月在线」的职责之一 且个人认为&#xff0c…...

对象回调初步研究

_OBJECT_TYPE结构分析 在介绍什么是对象回调前&#xff0c;首先要熟悉下结构 以我们上篇线程回调介绍过的导出的PsProcessType 结构为例&#xff0c;用_OBJECT_TYPE这个结构来解析它&#xff0c;0x80处就是今天要介绍的回调链表&#xff0c;但是先不着急&#xff0c;先把目光…...

Java数组Arrays操作全攻略

Arrays类的概述 Java中的Arrays类位于java.util包中&#xff0c;提供了一系列静态方法用于操作数组&#xff08;如排序、搜索、填充、比较等&#xff09;。这些方法适用于基本类型数组和对象数组。 常用成员方法及代码示例 排序&#xff08;sort&#xff09; 对数组进行升序…...

ThreadLocal 源码

ThreadLocal 源码 此类提供线程局部变量。这些变量不同于它们的普通对应物&#xff0c;因为每个访问一个线程局部变量的线程&#xff08;通过其 get 或 set 方法&#xff09;都有自己独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段&#xff0c;这些类希望将…...