原文作者:jaypume
元の公開講座:https://www.bilibili.com/video/BV1hg4y1b78L
原文:https://github.com/jaypume/article/blob/main/sedna/边云协同AI框架Sedna源码解析/README.MD
自分の学習用に転載。あとで引きやすいように。
KubeEdge-Sedna 概要
Sedna は KubeEdge SIG AI で孵化した辺雲協同 AI プロジェクト。KubeEdge の辺雲協同の上で、連合推論、増分学習、連邦学習、生涯学習などの、クラウドとエッジをまたぐ協同訓練・推論ができる。TensorFlow / Pytorch / MindSpore などよく使う AI フレームワークをサポートし、既存の AI アプリをそのまま載せ、辺雲の訓練と推論をすぐ始められる。コスト、モデル性能、データプライバシーの面で上げられる。
プロジェクト:
https://github.com/kubeedge/sedna
ドキュメント:
全体アーキ
Sedna の辺雲協同は KubeEdge の次の能力の上にある。
- 辺雲をまたぐアプリの統一オーケストレーション
- Router:管理面の高信頼メッセージチャネル
- EdgeMesh:データ面のマイクロサービス発見とトラフィック

基本コンポーネント:
- GlobalManager
- 辺雲協同 AI タスクの統一管理
- 辺雲をまたぐ協同
- 中央の設定
- LocalController
- 辺雲協同 AI タスクのローカル流れ
- ローカルの汎用管理:モデル、データセット、状態同期など
- Lib
- AI/アプリ開発者向け。辺雲協同 AI をアプリに出す
- Worker
- 訓練または推論。既存 AI フレームワーク上のプログラム
- 特性ごとに worker 組が違い、エッジにもクラウドにも置け、協同する
工程ディレクトリ
| ディレクトリ | 説明 |
|---|---|
| .github | Sedna GitHub CICD |
| LICENSES | Sedna と vendor のライセンス |
| build | GM/LC の Dockerfile、生成 CRD yaml、CRD サンプル |
| cmd | GM/LC の起動 |
| components | 監視と可視化 |
| docs | proposals とインストール |
| examples | 協同推論、増分、生涯、連邦の例 |
| hack | コード生成など開発用 |
| lib | Sedna Library。辺雲協同 AI 用 Python 依存 |
| pkg | API、生成 client-go、GM/LC のコア |
| scripts | 利用者向けインストール |
| test | E2E とテストツール |
| vendor | 第三者ソース |
Sedna 管理面ソース解析(Go)
GM: Global Manager
GM、ひとつの K8S operator
operator とは?
An Operator is an application-specific controller that extends the Kubernetes API to create, configure and manage instances of complex stateful applications on behalf of a Kubernetes user. It builds upon the basic Kubernetes resource and controller concepts, but also includes domain or application-specific knowledge to automate common tasks better managed by computers. 1
Sedna は辺雲協同 AI アプリで、worker の起動パラメータ、協同の仕方、状態の流れを制御する。だから:Sedna GM は「辺雲協同 AI アプリ」という領域のコントローラだ。
The following components form the three main parts of an operator:
- API: The data that describes the operand’s configuration. The API includes:
- Custom resource definition (CRD), which defines a schema of settings available for configuring the operand.
- Programmatic API, which defines the same data schema as the CRD and is implemented using the operator’s programming language, such as Go.
- Custom resource (CR), which specifies values for the settings defined by the CRD; these values describe the configuration of an operand.
- Controller: The brains of the operator. The controller creates managed resources based on the description in the custom resource; controllers are implemented using the operator’s programming language, such as Go. 2
Red Hat の定義から、k8s operator の要点は CRD、API、CR、Controller。
Sedna GM Operator の図:

以降はこの部品ごと。Controller が主な制御ロジック。
CR
Sedna は辺雲協同推論、増分学習、生涯学習、連邦学習を支える。コードを読みやすくするため、本稿は生涯学習で見る。他の三つは共通が多いので類推できる。
CR サンプル
生涯学習の CR サンプル。kubectl で対応するリソースを作れる。手順はここ。主なフィールド:
- dataset:データセットオブジェクト名。データセットも CR。
- trainSpec:訓練 worker の起動パラメータ。イメージ、環境変数など。
- trigger:訓練 worker を起こす条件。
- evalSpec:評価 worker の起動パラメータ。
- deploySpec:推論 worker の起動パラメータ。
- outputDir:訓練で出たモデルの出力パス。
build/crd-samples/sedna/lifelonglearningjobv1alpha1.yaml
apiVersion: sedna.io/v1alpha1
kind: LifelongLearningJob
metadata:
name: atcii-classifier-demo
spec:
dataset:
name: "lifelong-dataset"
trainProb: 0.8
trainSpec:
template:
spec:
nodeName: "edge-node"
containers:
- image: kubeedge/sedna-example-lifelong-learning-atcii-classifier:v0.3.0
name: train-worker
imagePullPolicy: IfNotPresent
args: ["train.py"]
env:
- name: "early_stopping_rounds"
value: "100"
- name: "metric_name"
value: "mlogloss"
trigger:
checkPeriodSeconds: 60
timer:
start: 02:00
end: 24:00
condition:
operator: ">"
threshold: 500
metric: num_of_samples
evalSpec:
template:
spec:
nodeName: "edge-node"
containers:
- image: kubeedge/sedna-example-lifelong-learning-atcii-classifier:v0.3.0
name: eval-worker
imagePullPolicy: IfNotPresent
args: ["eval.py"]
env:
- name: "metrics"
value: "precision_score"
- name: "metric_param"
value: "{'average': 'micro'}"
- name: "model_threshold"
value: "0.5"
deploySpec:
template:
spec:
nodeName: "edge-node"
containers:
- image: kubeedge/sedna-example-lifelong-learning-atcii-classifier:v0.3.0
name: infer-worker
imagePullPolicy: IfNotPresent
args: ["inference.py"]
env:
- name: "UT_SAVED_URL"
value: "/ut_saved_url"
- name: "infer_dataset_url"
value: "/data/testData.csv"
volumeMounts:
- name: utdir
mountPath: /ut_saved_url
- name: inferdata
mountPath: /data/
resources:
limits:
memory: 2Gi
volumes:
- name: utdir
hostPath:
path: /lifelong/unseen_task/
type: DirectoryOrCreate
- name: inferdata
hostPath:
path: /data/
type: DirectoryOrCreate
outputDir: "/output"CRD
CRD は CR のテンプレート。クラスタで CR を作る前に CRD を宣言する。yaml は手書きでも自動生成でもよい。複雑なものは k8s のツールで生成するのがよい。Sedna は kubebuilder の controller-gen。make crds で build/crds/ を更新。Makefile の crds: controller-gen。
CRD を定義するには group、version、kind が要る。GVK。CR オブジェクト自体は Resource。OO なら Resource は Object、Kind は Class。Resource は Kind のインスタンス。生涯学習の GVR / GVK:
| Group | Version | Resource | Kind | |
|---|---|---|---|---|
| CRD | apiextensions.k8s.io | v1 | lifelonglearningjobs.sedna.io | CustomResourceDefinition |
| CR | sedna.io | v1alpha1 | lifelonglearningjob | LifelongLearningJob |
K8S ではリソースは REST URI で組む:

この規則が分かれば、kubectl や client-go なしでも REST URI で触れる。例:
REST で生涯学習 CRD:
curl -k --cert ./client.crt --key ./client.key https://127.0.0.1:5443/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/lifelonglearningjobs.sedna.ioREST で生涯学習 CR 一覧:
curl -k --cert ./client.crt --key ./client.key https://127.0.0.1:5443/apis/sedna.io/v1alpha1/lifelonglearningjobs公式 k8s client がない言語なら、この REST を包めばよい。
Sedna 生涯学習 CRD で見るフィールド:
apiVersion: apiextensions.k8s.io/v1,いまの CRD はすべて apiextensions.k8s.io/v1 から拡張kind: CustomResourceDefinition,すべての CRD はこの Kind を継承spec.group: sedna.io,カスタムリソースの Group は sedna.iospec.names.kind: LifelongLearningJob,追加した型spec.names.shortNames: - ll,kubectl ではllで引ける
build/crds/sedna.io_lifelonglearningjobs.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.4.1
creationTimestamp: null
name: lifelonglearningjobs.sedna.io
spec:
group: sedna.io
names:
kind: LifelongLearningJob
listKind: LifelongLearningJobList
plural: lifelonglearningjobs
shortNames:
- ll
singular: lifelonglearningjob
scope: Namespaced
versions:
- name: v1alpha1
...
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []API
CRD は自動生成だと書いた。元の API 定義はどこか。
pkg/apis/sedna/v1alpha1/lifelonglearningjob_types.go
package v1alpha1
import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// 这里展示了
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:shortName=ll
// +kubebuilder:subresource:status
// 整体的LifelongLearningJob的API定义,主要包含Spec和Status定义,分别代表期望状态和实际状态。
type LifelongLearningJob struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
Spec LLJobSpec `json:"spec"`
Status LLJobStatus `json:"status,omitempty"`
}
// 在创建LifelongLearningJob时候需要配置的参数;如果需要扩展终身学习字段的接口,可以在这里修改。
type LLJobSpec struct {
Dataset LLDataset `json:"dataset"`
TrainSpec LLTrainSpec `json:"trainSpec"`
EvalSpec LLEvalSpec `json:"evalSpec"`
DeploySpec LLDeploySpec `json:"deploySpec"`
// the credential referer for OutputDir
CredentialName string `json:"credentialName,omitempty"`
OutputDir string `json:"outputDir"`
}
type LLDataset struct {
Name string `json:"name"`
TrainProb float64 `json:"trainProb"`
}
// 剩下还有一些结构体定义省略了。この断片で注意:
// +kubebuilder...:kubebuilder など生成ツール向けtype LifelongLearningJob struct{...}:全体 API。Spec が期待、Status が実際type LLJobSpec struct {...}:CR 作成時のパラメータ。フィールドを足すならここ
協同推論、増分、連邦の API も pkg/apis/sedna/v1alpha1/。
client-go の更新
*_types.go を足したり変えたら:
bash hack/update-codegen.sh生成物は pkg/client:
➜ pkg tree client -L 2
client
├── clientset
│ └── versioned
├── informers
│ └── externalversions
└── listers
└── sednaclient-go はあとの Controller で使う。
CRD 定義の更新
*_types.go を変えたら:
make crdsyaml は build/crds。クラスタでも kubectl apply し直す。
Controller
生涯学習の主な制御は pkg/globalmanager/controllers/lifelonglearning/lifelonglearningjob.go。訓練・評価 Worker をいつ起こすか、パラメータをどうエッジへ同期するか。
その前の呼び出しは次の疑似コード:
cmd/sedna-gm/sedna-gm.go/main() 【1】
pkg/globalmanager/controllers/manager.go/New() 【2】读取GM配置文件。
pkg/globalmanager/controllers/manager.go/Start() 【3】启动GM进程。
- clientset.NewForConfig():【4】调用client-go生成了Sedna CRD client。
- NewUpstreamController():【5】创建UpstreamController,每个GM进程有一个UpstreamController
- uc.Run(stopCh):启动一个for循环协程,来处理
- pkg/globalmanager/controllers/upstream.go/syncEdgeUpdate()
- NewRegistry():【6】注册所有controller。
- f.SetDownstreamSendFunc()【7】
-> pkg/globalmanager/controllers/lifelonglearning/downstream.go
- f.SetUpstreamHandler()【8】
-> pkg/globalmanager/controllers/lifelonglearning/upstream.go/updateFromEdge()
- f.Run()【9】
- ws.ListenAndServe() 【10】LifelongLearningJob Controller を、上の 1〜10 の番号で見る。
【1】main
sedna-gm.go が GM の入口。ログ、app.NewControllerCommand() でパラメータ、controller 起動。
cmd/sedna-gm/sedna-gm.go
func main() {
rand.Seed(time.Now().UnixNano())
command := app.NewControllerCommand()
logs.InitLogs()
defer logs.FlushLogs()
if err := command.Execute(); err != nil {
os.Exit(1)
}
}【2】GM 設定
K8S クラスタ、Websocket のアドレスとポート、KB のアドレスなど。
pkg/globalmanager/controllers/manager.go
// New creates the controller manager
func New(cc *config.ControllerConfig) *Manager {
config.InitConfigure(cc)
return &Manager{
Config: cc,
}
}pkg/globalmanager/config/config.go
// ControllerConfig indicates the config of controller
type ControllerConfig struct {
// KubeAPIConfig indicates the kubernetes cluster info which controller will connected
KubeConfig string `json:"kubeConfig,omitempty"`
// Master indicates the address of the Kubernetes API server. Overrides any value in KubeConfig.
// such as https://127.0.0.1:8443
// default ""
Master string `json:"master"`
// Namespace indicates which namespace the controller listening to.
// default ""
Namespace string `json:"namespace,omitempty"`
// websocket server config
// Since the current limit of kubeedge(1.5), GM needs to build the websocket channel for communicating between GM and LCs.
WebSocket WebSocket `json:"websocket,omitempty"`
// lc config to info the worker
LC LCConfig `json:"localController,omitempty"`
// kb config to info the worker
KB KBConfig `json:"knowledgeBaseServer,omitempty"`
// period config min resync period
// default 30s
MinResyncPeriodSeconds int64 `json:"minResyncPeriodSeconds,omitempty"`
}【3】GM 全体の初期化
Sedna CRD client、辺雲メッセージ処理のバインドと起動、各特性の controller、websocket 待ち受け。
pkg/globalmanager/controllers/manager.go
// Start starts the controllers it has managed
func (m *Manager) Start() error {
...
// 初始化Sedna CRD client,Controller会监听Sedna CR 增删改查的变化,并执行对应的处理逻辑。
sednaClient, err := clientset.NewForConfig(kubecfg)
...
sednaInformerFactory := sednainformers.NewSharedInformerFactoryWithOptions(sednaClient, genResyncPeriod(minResyncPeriod), sednainformers.WithNamespace(namespace))
// 初始化UpstreamController,用于处理边缘LC上传的消息
uc, _ := NewUpstreamController(context)
downstreamSendFunc := messagelayer.NewContextMessageLayer().SendResourceObject
stopCh := make(chan struct{})
go uc.Run(stopCh)
// 针对每个特性(协同推理、终身学习等),绑定对应的消息处理函数
for name, factory := range NewRegistry() {
...
f.SetDownstreamSendFunc(downstreamSendFunc)
f.SetUpstreamHandler(uc.Add)
...
// 启动各个特性对应controller
go f.Run(stopCh)
}
...
// 启动整体GM的websocket,默认监听在0.0.0.0:9000这个端口地址
ws := websocket.NewServer(addr)
...
}【4】CRD client 初期化
clientset.NewForConfig() の元は pkg/client/clientset/versioned/clientset.go。client-go が CRD から生成。Go で CRUD。
LifelongLearningJob Controller の初期化はこの client に依存する。やっていること:
- LifelongLearningJob の Informer。controller から見た api-server の「ローカルキャッシュ」。読み負荷を減らす。
- k8s client、sedna client、GM 共通設定。
- Add / Update / Delete のコールバック。
pkg/globalmanager/controllers/lifelonglearning/lifelonglearningjob.go
// New creates a new LifelongLearningJob controller that keeps the relevant pods
// in sync with their corresponding LifelongLearningJob objects.
func New(cc *runtime.ControllerContext) (runtime.FeatureControllerI, error) {
cfg := cc.Config
podInformer := cc.KubeInformerFactory.Core().V1().Pods()
// 获取LifelongLearningJob的Informer
jobInformer := cc.SednaInformerFactory.Sedna().V1alpha1().LifelongLearningJobs()
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: cc.KubeClient.CoreV1().Events("")})
// 配置LifelongLearningJob Controller的参数
jc := &Controller{
kubeClient: cc.KubeClient,
client: cc.SednaClient.SednaV1alpha1(),
queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(runtime.DefaultBackOff, runtime.MaxBackOff), Name),
cfg: cfg,
}
// 绑定LifelongLearningJob CRD资源的Add、Update、Delete对应事件的回调函数。
jobInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
jc.enqueueController(obj, true)
jc.syncToEdge(watch.Added, obj)
},
UpdateFunc: func(old, cur interface{}) {
jc.enqueueController(cur, true)
jc.syncToEdge(watch.Added, cur)
},
DeleteFunc: func(obj interface{}) {
jc.enqueueController(obj, true)
jc.syncToEdge(watch.Deleted, obj)
},
})
jc.jobLister = jobInformer.Lister()
jc.jobStoreSynced = jobInformer.Informer().HasSynced
// 绑定Pod对应的增删改对应事件的回调函数。
podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: jc.addPod,
UpdateFunc: jc.updatePod,
DeleteFunc: jc.deletePod,
})
jc.podStore = podInformer.Lister()
jc.podStoreSynced = podInformer.Informer().HasSynced
return jc, nil
}他モジュールからの参照:

【5】メッセージ処理の初期化
uc.Run() で UpstreamController。エッジからの全メッセージ。
for で context.upstreamChannel を聞く。来たら uc.updateHandlers[kind] で handler を取り、呼ぶ。map には協同推論、増分、連邦、生涯の updateHandlers。
pkg/globalmanager/controllers/upstream.go
// syncEdgeUpdate receives the updates from edge and syncs these to k8s.
func (uc *UpstreamController) syncEdgeUpdate() {
for {
select {
case <-uc.messageLayer.Done():
klog.Info("Stop sedna upstream loop")
return
default:
}
update, err := uc.messageLayer.ReceiveResourceUpdate()
...
handler, ok := uc.updateHandlers[kind]
if ok {
err := handler(name, namespace, operation, update.Content)
...
}
}
}ReceiveFromEdge はブロックするチャネル。エッジ LC からの nodeMessage。
pkg/globalmanager/messagelayer/ws/context.go
// ReceiveResourceUpdate receives and handles the update
func (cml *ContextMessageLayer) ReceiveResourceUpdate() (*ResourceUpdateSpec, error) {
nodeName, msg, err := wsContext.ReceiveFromEdge()
...
}【6】Controller 登録
NewRegistry() が各特性の New を登録。新しい辺雲特性を足すならここに New を足す。
pkg/globalmanager/controllers/registry.go
func NewRegistry() Registry {
return Registry{
ji.Name: ji.New,
fe.Name: fe.New,
fl.Name: fl.New,
il.Name: il.New,
ll.Name: ll.New,
reid.Name: reid.New,
va.Name: va.New,
dataset.Name: dataset.New,
objs.Name: objs.New,
}
}【7】クラウドからエッジへ
f.SetDownstreamSendFunc() が各特性の syncToEdge()。
生涯学習では:
- Dataset CRD に書いてある Node 名を取る。
- 訓練・評価・デプロイのノード名は Annotation。
- ステージに応じて違うノードへ送る。
pkg/globalmanager/controllers/lifelonglearning/downstream.go
func (c *Controller) syncToEdge(eventType watch.EventType, obj interface{}) error {
// 获取到对应的数据集指定的节点(Dataset CRD对象中有一个字段记录了Node名称)
ds, err := c.client.Datasets(job.Namespace).Get(context.TODO(), dataName, metav1.GetOptions{})
// 获取到训练、评估、部署对应的节点名称
getAnnotationsNodeName := func(nodeName sednav1.LLJobStage) string {
return runtime.AnnotationsKeyPrefix + string(nodeName)
}
ann := job.GetAnnotations()
if ann != nil {
trainNodeName = ann[getAnnotationsNodeName(sednav1.LLJobTrain)]
evalNodeName = ann[getAnnotationsNodeName(sednav1.LLJobEval)]
deployNodeName = ann[getAnnotationsNodeName(sednav1.LLJobDeploy)]
}
...
// 根据LifelongLearningJob所处阶段不同,发送消息到不同的节点上
switch jobStage {
case sednav1.LLJobTrain:
doJobStageEvent(trainNodeName)
case sednav1.LLJobEval:
doJobStageEvent(evalNodeName)
case sednav1.LLJobDeploy:
doJobStageEvent(deployNodeName)
}
return nil
}【8】エッジからクラウドへ
f.SetUpstreamHandler() が各特性の updateFromEdge()。
生涯学習では:
- エッジのタスク完了に応じて LifelongLearningJob 全体の状態を変える。
- その状態を k8s に書き戻す。CR の Status。
- エッジのメッセージ(json)をパース。例:
GM が受ける例:
{
"phase": "train",
"status": "completed",
"output": {
"models": [{
"classes": ["road", "fence"],
"current_metric": null,
"format": "pkl",
"metrics": null,
"url": "/output/train/1/index.pkl"
}],
"ownerInfo": null
}
}pkg/globalmanager/controllers/lifelonglearning/upstream.go
// updateFromEdge syncs the edge updates to k8s
func (c *Controller) updateFromEdge(name, namespace, operation string, content []byte) error {
var jobStatus struct {
Phase string `json:"phase"`
Status string `json:"status"`
}
// 把边缘消息结构体进行解析。
err := json.Unmarshal(content, &jobStatus)
...
cond := sednav1.LLJobCondition{
Status: v1.ConditionTrue,
LastHeartbeatTime: metav1.Now(),
LastTransitionTime: metav1.Now(),
Data: string(condDataBytes),
Message: "reported by lc",
}
// 根据不同的边缘节点任务状态实现,变更当前LifelongLearningJob的整体状态
switch strings.ToLower(jobStatus.Status) {
case "ready":
cond.Type = sednav1.LLJobStageCondReady
case "completed":
cond.Type = sednav1.LLJobStageCondCompleted
case "failed":
cond.Type = sednav1.LLJobStageCondFailed
case "waiting":
cond.Type = sednav1.LLJobStageCondWaiting
default:
return fmt.Errorf("invalid condition type: %v", jobStatus.Status)
}
// 将当前LifelongLearningJob的整体状态写回k8s,也就是LifelongLearningJob这个CR的Status字段。
err = c.appendStatusCondition(name, namespace, cond)
...
}【9】Controller のコア
f.run() が各特性の処理。LifelongLearningJob の run():
WaitForNamedCacheSync で Pod と LifelongLearningJob が Informer に載るまで待つ。載ったら worker を指定数起こす。
pkg/globalmanager/controllers/lifelonglearning/lifelonglearningjob.go
// Run starts the main goroutine responsible for watching and syncing jobs.
func (c *Controller) Run(stopCh <-chan struct{}) {
workers := 1
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
klog.Infof("Starting %s controller", Name)
defer klog.Infof("Shutting down %s controller", Name)
if !cache.WaitForNamedCacheSync(Name, stopCh, c.podStoreSynced, c.jobStoreSynced) {
klog.Errorf("failed to wait for %s caches to sync", Name)
return
}
klog.Infof("Starting %s workers", Name)
for i := 0; i < workers; i++ {
go wait.Until(c.worker, time.Second, stopCh)
}
<-stopCh
}c.worker は processNextWorkItem() でオブジェクトを処理する。
pkg/globalmanager/controllers/lifelonglearning/lifelonglearningjob.go
// worker runs a worker thread that just dequeues items, processes them, and marks them done.
// It enforces that the syncHandler is never invoked concurrently with the same key.
func (c *Controller) worker() {
for c.processNextWorkItem() {
}
}processNextWorkItem() は c.sync() で特性ロジック。
pkg/globalmanager/controllers/lifelonglearning/lifelonglearningjob.go
func (c *Controller) sync(key string) (bool, error) {
//省略了部分代码
ns, name, err := cache.SplitMetaNamespaceKey(key)
sharedJob, err := c.jobLister.LifelongLearningJobs(ns).Get(name)
// if job was finished previously, we don't want to redo the termination
if IsJobFinished(&job) {
return true, nil
}
// transit this job's state machine
needUpdated, err = c.transitJobState(&job)
if needUpdated {
if err := c.updateJobStatus(&job); err != nil {
return forget, err
}
if jobFailed && !IsJobFinished(&job) {
// returning an error will re-enqueue LifelongLearningJob after the backoff period
return forget, fmt.Errorf("failed pod(s) detected for lifelonglearningjob key %q", key)
}
forget = true
}
return forget, err
}sync は具体的な job を処理する:
SplitMetaNamespaceKeyで namespace と namec.jobListerでオブジェクトtransitJobStateで訓練・評価・デプロイのどれか- Status が変わったら
c.updateJobStatus()で k8s に書き、kubectl で今の段階やモデルパスが見える - 失敗などの例外
// transit this job's state machine
needUpdated, err = c.transitJobState(&job)transitJobState() が状態機械。訓練・評価・デプロイをいつ起動・停止するか。図:

【10】websocket 待ち受け
【8】のエッジメッセージを受ける websocket。既定 0.0.0.0:9000。
pkg/globalmanager/controllers/manager.go
addr := fmt.Sprintf("%s:%d", m.Config.WebSocket.Address, m.Config.WebSocket.Port)
ws := websocket.NewServer(addr)
err = ws.ListenAndServe()LC: Local Controller
LC はエッジに置く。ローカルのタスク管理とメッセージプロキシ。入口は cmd/sedna-lc/sedna-lc.go。入口の話は GM を参照。ローカルタスク管理の登録:
cmd/sedna-lc/app/server.go
// runServer runs server
func runServer() {
c := gmclient.NewWebSocketClient(Options)
if err := c.Start(); err != nil {
return
}
dm := dataset.New(c, Options)
mm := model.New(c)
jm := jointinference.New(c)
fm := federatedlearning.New(c)
im := incrementallearning.New(c, dm, mm, Options)
lm := lifelonglearning.New(c, dm, Options)
s := server.New(Options)
for _, m := range []managers.FeatureManager{
dm, mm, jm, fm, im, lm,
} {
s.AddFeatureManager(m)
c.Subscribe(m)
err := m.Start()
if err != nil {
klog.Errorf("failed to start manager %s: %v",
m.GetName(), err)
return
}
klog.Infof("manager %s is started", m.GetName())
}
s.ListenAndServe()
}ローカルタスク管理
Manager がエッジのタスク管理。形:
pkg/localcontroller/managers/lifelonglearning/lifelonglearningjob.go
// LifelongLearningJobManager defines lifelong-learning-job Manager
type Manager struct {
Client clienttypes.ClientI
WorkerMessageChannel chan workertypes.MessageContent
DatasetManager *dataset.Manager
LifelongLearningJobMap map[string]*Job
VolumeMountPrefix string
}startJob() の流れ:
- エッジに同期された Dataset を監視。サンプル数が閾値なら訓練を起こす、など。
- 今の段階に応じて訓練・評価・デプロイ。ローカルでは直接起こさず、状態を GM に上げ、GM がスケジュールする。
pkg/localcontroller/managers/lifelonglearning/lifelonglearningjob.go
// startJob starts a job
func (lm *Manager) startJob(name string) {
...
// 监控并处理同步到边缘的Dataset对象。
go lm.handleData(job)
tick := time.NewTicker(JobIterationIntervalSeconds * time.Second)
for {
// 根据当前任务不同阶段,触发不同阶段的训练、评估、部署任务。
select {
case <-job.JobConfig.Done:
return
case <-tick.C:
cond := lm.getLatestCondition(job)
jobStage := cond.Stage
switch jobStage {
case sednav1.LLJobTrain:
err = lm.trainTask(job)
case sednav1.LLJobEval:
err = lm.evalTask(job)
case sednav1.LLJobDeploy:
err = lm.deployTask(job)
default:
klog.Errorf("invalid phase: %s", jobStage)
continue
}
...
}
}
}ほかにデータセット監視、モデルダウンロード、ジョブのローカル DB バックアップなど。
メッセージプロキシ
状態をクラウドへ送るほか、LC は 0.0.0.0:9100 で HTTP Server を上げ、Lib からのメッセージをまとめて GM へ送る。ルートと処理:
pkg/localcontroller/server/server.go
// register registers api
func (s *Server) register(container *restful.Container) {
ws := new(restful.WebService)
ws.Path(fmt.Sprintf("/%s", constants.ServerRootPath)).
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML)
ws.Route(ws.POST("/workers/{worker-name}/info").
To(s.messageHandler).
Doc("receive worker message"))
container.Add(ws)
}pkg/localcontroller/server/server.go
// messageHandler handles message from the worker
func (s *Server) messageHandler(request *restful.Request, response *restful.Response) {
var err error
workerName := request.PathParameter("worker-name")
workerMessage := workertypes.MessageContent{}
err = request.ReadEntity(&workerMessage)
if workerMessage.Name != workerName || err != nil {
var msg string
if workerMessage.Name != workerName {
msg = fmt.Sprintf("worker name(name=%s) in the api is different from that(name=%s) in the message body",
workerName, workerMessage.Name)
} else {
msg = fmt.Sprintf("read worker(name=%s) message body failed, error: %v", workerName, err)
}
klog.Errorf(msg)
err = s.reply(response, http.StatusBadRequest, msg)
if err != nil {
klog.Errorf("reply messge to worker(name=%s) failed, error: %v", workerName, err)
}
}
if m, ok := s.fmm[workerMessage.OwnerKind]; ok {
m.AddWorkerMessage(workerMessage)
}
err = s.reply(response, http.StatusOK, "OK")
if err != nil {
klog.Errorf("reply message to worker(name=%s) failed, error: %v", workerName, err)
return
}
}Sedna Lib ソース解析(Python)
Lib は AI/アプリ開発者向けの Python ライブラリ。既存コードを辺雲協同に変えるため。
ディレクトリ:
➜ sedna tree lib -L 2
lib
├── __init__.py
├── MANIFEST.in
├── OWNERS
├── requirements.dev.txt
├── requirements.txt // Sedna Python的依赖
├── sedna
│ ├── algorithms // 边云协同算法
│ ├── backend // 支持的后端,tensorflow/pytorch
│ ├── common
│ ├── core // 主要特性的实现逻辑
│ ├── datasources // 支持的数据源格式,比如txt、csv等
│ ├── __init__.py
│ ├── README.md
│ ├── service // 需要启动server的组件,比如kb等
│ ├── VERSION
│ └── __version__.py
└── setup.py各部の典型コード:
core
core はユーザのコールバックを包む。下の train は、ユーザが包んだ tensorflow / pytorch / mindspore の train を呼ぶ。
- 後処理を設定。
- クラウド知識ベースで訓練・推論。
- クラウド KB を更新。生涯学習では新しいモデルとサンプルを置き、更新し続ける。
- 訓練の実行状況を LC へ。完了したか、指標はいくらか。
lib/sedna/core/lifelong_learning/lifelong_learning.py
def train(self, train_data,
valid_data=None,
post_process=None,
**kwargs):
is_completed_initilization = \
str(Context.get_parameters("HAS_COMPLETED_INITIAL_TRAINING",
"false")).lower()
if is_completed_initilization == "true":
return self.update(train_data,
valid_data=valid_data,
post_process=post_process,
**kwargs)
# 配置后处理函数
callback_func = None
if post_process is not None:
callback_func = ClassFactory.get_cls(
ClassType.CALLBACK, post_process)
res, seen_task_index = \
self.cloud_knowledge_management.seen_estimator.train(
train_data=train_data,
valid_data=valid_data,
**kwargs
)
# 调用云端知识库进行训练、或推理
unseen_res, unseen_task_index = \
self.cloud_knowledge_management.unseen_estimator.train()
# 更新云端知识库
task_index = dict(
seen_task=seen_task_index,
unseen_task=unseen_task_index)
task_index_url = FileOps.dump(
task_index, self.cloud_knowledge_management.local_task_index_url)
task_index = self.cloud_knowledge_management.update_kb(task_index_url)
res.update(unseen_res)
...
# 将当前训练任务执行的情况发送给LC,比如训练任务是否完成、训练后的指标是多少
self.report_task_info(
None, K8sResourceKindStatus.COMPLETED.value, task_info_res)
self.log.info(f"Lifelong learning Train task Finished, "
f"KB index save in {task_index}")
return callback_func(self.estimator, res) if callback_func else res
...backend
MSBackend は Sedna が支えるバックエンド、MindSpore。フレームワークに典型的な train / predict / evaluate があれば、Sedna Lib はそれを backend にでき、既存 AI コードを薄く包んで辺雲協同にできる。
lib/sedna/backend/mindspore/__init__.py
class MSBackend(BackendBase):
def __init__(self, estimator, fine_tune=True, **kwargs):
super(MSBackend, self).__init__(estimator=estimator,
fine_tune=fine_tune,
**kwargs)
self.framework = "mindspore"
if self.use_npu:
context.set_context(mode=context.GRAPH_MODE,
device_target="Ascend")
elif self.use_cuda:
context.set_context(mode=context.GRAPH_MODE,
device_target="GPU")
else:
context.set_context(mode=context.GRAPH_MODE,
device_target="CPU")
if callable(self.estimator):
self.estimator = self.estimator()
def train(self, train_data, valid_data=None, **kwargs):
if callable(self.estimator):
self.estimator = self.estimator()
if self.fine_tune and FileOps.exists(self.model_save_path):
self.finetune()
self.has_load = True
varkw = self.parse_kwargs(self.estimator.train, **kwargs)
return self.estimator.train(train_data=train_data,
valid_data=valid_data,
**varkw)
def predict(self, data, **kwargs):
if not self.has_load:
self.load()
varkw = self.parse_kwargs(self.estimator.predict, **kwargs)
return self.estimator.predict(data=data, **varkw)
def evaluate(self, data, **kwargs):
if not self.has_load:
self.load()
varkw = self.parse_kwargs(self.estimator.evaluate, **kwargs)
return self.estimator.evaluate(data, **varkw)datasource
datasource はよくあるデータセット形式の処理を包む。こうすれば
lib/sedna/datasources/__init__.py
class CSVDataParse(BaseDataSource, ABC):
"""
csv file which contain Structured Data parser
"""
# 提供了方便的数据集解析函数,
def parse(self, *args, **kwargs):
x_data = []
y_data = []
label = kwargs.pop("label") if "label" in kwargs else ""
usecols = kwargs.get("usecols", "")
if usecols and isinstance(usecols, str):
usecols = usecols.split(",")
if len(usecols):
if label and label not in usecols:
usecols.append(label)
kwargs["usecols"] = usecols
for f in args:
if isinstance(f, (dict, list)):
res = self.parse_json(f, **kwargs)
else:
if not (f and FileOps.exists(f)):
continue
res = pd.read_csv(f, **kwargs)
if self.process_func and callable(self.process_func):
res = self.process_func(res)
if label:
if label not in res.columns:
continue
y = res[label]
y_data.append(y)
res.drop(label, axis=1, inplace=True)
x_data.append(res)
if not x_data:
return
self.x = pd.concat(x_data)
self.y = pd.concat(y_data)algorithms
辺雲協同 AI には、その場向けのアルゴリズムが要る。難例認識もいくつか入っている。下の交差エントロピー閾値は、エッジのモデルが自信がないときにサンプルを拾える。
基礎アルゴリズムを入れるだけが目的ではなく、辺雲の枠の上で実用的なアルゴリズムを足し、訓練・推論全体を上げること。それが Sedna Lib のこの枠がやりたいことだ。
lib/sedna/algorithms/hard_example_mining/hard_example_mining.py
@ClassFactory.register(ClassType.HEM, alias="CrossEntropy")
class CrossEntropyFilter(BaseFilter, abc.ABC):
"""
**Object detection** Hard samples discovery methods named `CrossEntropy`
Parameters
----------
threshold_cross_entropy: float
hard coefficient threshold score to filter img, default to 0.5.
"""
def __init__(self, threshold_cross_entropy=0.5, **kwargs):
self.threshold_cross_entropy = float(threshold_cross_entropy)
def __call__(self, infer_result=None) -> bool:
"""judge the img is hard sample or not.
Parameters
----------
infer_result: array_like
prediction classes list, such as
[class1-score, class2-score, class2-score,....],
where class-score is the score corresponding to the class,
class-score value is in [0,1], who will be ignored if its
value not in [0,1].
Returns
-------
is hard sample: bool
`True` means hard sample, `False` means not.
"""
if not infer_result:
# if invalid input, return False
return False
log_sum = 0.0
data_check_list = [class_probability for class_probability
in infer_result
if self.data_check(class_probability)]
if len(data_check_list) != len(infer_result):
return False
for class_data in data_check_list:
log_sum += class_data * math.log(class_data)
confidence_score = 1 + 1.0 * log_sum / math.log(
len(infer_result))
return confidence_score < self.threshold_cross_entropy