You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

jointinferenceservice.go 18 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. /*
  2. Copyright 2021 The KubeEdge Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package jointinference
  14. import (
  15. "context"
  16. "encoding/json"
  17. "fmt"
  18. "strconv"
  19. "time"
  20. v1 "k8s.io/api/core/v1"
  21. "k8s.io/apimachinery/pkg/api/errors"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. utilrand "k8s.io/apimachinery/pkg/util/rand"
  24. utilruntime "k8s.io/apimachinery/pkg/util/runtime"
  25. "k8s.io/apimachinery/pkg/util/wait"
  26. "k8s.io/client-go/kubernetes"
  27. "k8s.io/client-go/kubernetes/scheme"
  28. v1core "k8s.io/client-go/kubernetes/typed/core/v1"
  29. corelisters "k8s.io/client-go/listers/core/v1"
  30. "k8s.io/client-go/tools/cache"
  31. "k8s.io/client-go/tools/record"
  32. "k8s.io/client-go/util/workqueue"
  33. "k8s.io/klog/v2"
  34. k8scontroller "k8s.io/kubernetes/pkg/controller"
  35. sednav1 "github.com/kubeedge/sedna/pkg/apis/sedna/v1alpha1"
  36. sednaclientset "github.com/kubeedge/sedna/pkg/client/clientset/versioned/typed/sedna/v1alpha1"
  37. sednav1listers "github.com/kubeedge/sedna/pkg/client/listers/sedna/v1alpha1"
  38. "github.com/kubeedge/sedna/pkg/globalmanager/config"
  39. "github.com/kubeedge/sedna/pkg/globalmanager/runtime"
  40. )
  41. const (
  42. // Name is this controller name
  43. Name = "JointInference"
  44. // KindName is the kind name of CR this controller controls
  45. KindName = "JointInferenceService"
  46. )
  47. const (
  48. jointInferenceForEdge = "Edge"
  49. jointInferenceForCloud = "Cloud"
  50. bigModelPort = 5000
  51. )
  52. // Kind contains the schema.GroupVersionKind for this controller type.
  53. var Kind = sednav1.SchemeGroupVersion.WithKind(Name)
  54. // Controller ensures that all JointInferenceService objects
  55. // have corresponding pods to run their configured workload.
  56. type Controller struct {
  57. kubeClient kubernetes.Interface
  58. client sednaclientset.SednaV1alpha1Interface
  59. // podStoreSynced returns true if the pod store has been synced at least once.
  60. podStoreSynced cache.InformerSynced
  61. // A store of pods
  62. podStore corelisters.PodLister
  63. // serviceStoreSynced returns true if the jointinferenceservice store has been synced at least once.
  64. serviceStoreSynced cache.InformerSynced
  65. // A store of service
  66. serviceLister sednav1listers.JointInferenceServiceLister
  67. // JointInferenceServices that need to be updated
  68. queue workqueue.RateLimitingInterface
  69. recorder record.EventRecorder
  70. cfg *config.ControllerConfig
  71. }
  72. // Run starts the main goroutine responsible for watching and syncing services.
  73. func (c *Controller) Run(stopCh <-chan struct{}) {
  74. workers := 1
  75. defer utilruntime.HandleCrash()
  76. defer c.queue.ShutDown()
  77. klog.Infof("Starting %s controller", Name)
  78. defer klog.Infof("Shutting down %s controller", Name)
  79. if !cache.WaitForNamedCacheSync(Name, stopCh, c.podStoreSynced, c.serviceStoreSynced) {
  80. klog.Errorf("failed to wait for %s caches to sync", Name)
  81. return
  82. }
  83. klog.Infof("Starting %s workers", Name)
  84. for i := 0; i < workers; i++ {
  85. go wait.Until(c.worker, time.Second, stopCh)
  86. }
  87. <-stopCh
  88. }
  89. // enqueueByPod enqueues the jointInferenceService object of the specified pod.
  90. func (c *Controller) enqueueByPod(pod *v1.Pod, immediate bool) {
  91. controllerRef := metav1.GetControllerOf(pod)
  92. if controllerRef == nil {
  93. return
  94. }
  95. if controllerRef.Kind != Kind.Kind {
  96. return
  97. }
  98. service, err := c.serviceLister.JointInferenceServices(pod.Namespace).Get(controllerRef.Name)
  99. if err != nil {
  100. return
  101. }
  102. if service.UID != controllerRef.UID {
  103. return
  104. }
  105. c.enqueueController(service, immediate)
  106. }
  107. // When a pod is created, enqueue the controller that manages it and update it's expectations.
  108. func (c *Controller) addPod(obj interface{}) {
  109. pod := obj.(*v1.Pod)
  110. if pod.DeletionTimestamp != nil {
  111. // on a restart of the controller, it's possible a new pod shows up in a state that
  112. // is already pending deletion. Prevent the pod from being a creation observation.
  113. c.deletePod(pod)
  114. return
  115. }
  116. // backoff to queue when PodFailed
  117. immediate := pod.Status.Phase != v1.PodFailed
  118. c.enqueueByPod(pod, immediate)
  119. }
  120. // When a pod is updated, figure out what joint inference service manage it and wake them up.
  121. func (c *Controller) updatePod(old, cur interface{}) {
  122. curPod := cur.(*v1.Pod)
  123. oldPod := old.(*v1.Pod)
  124. // no pod update, no queue
  125. if curPod.ResourceVersion == oldPod.ResourceVersion {
  126. return
  127. }
  128. c.addPod(curPod)
  129. }
  130. // deletePod enqueues the jointinferenceservice obj When a pod is deleted
  131. func (c *Controller) deletePod(obj interface{}) {
  132. pod, ok := obj.(*v1.Pod)
  133. // comment from https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/job_controller.go
  134. // When a delete is dropped, the relist will notice a pod in the store not
  135. // in the list, leading to the insertion of a tombstone object which contains
  136. // the deleted key/value. Note that this value might be stale. If the pod
  137. // changed labels the new jointinferenceservice will not be woken up till the periodic resync.
  138. if !ok {
  139. tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
  140. if !ok {
  141. klog.Warningf("couldn't get object from tombstone %+v", obj)
  142. return
  143. }
  144. pod, ok = tombstone.Obj.(*v1.Pod)
  145. if !ok {
  146. klog.Warningf("tombstone contained object that is not a pod %+v", obj)
  147. return
  148. }
  149. }
  150. c.enqueueByPod(pod, true)
  151. }
  152. // obj could be an *sednav1.JointInferenceService, or a DeletionFinalStateUnknown marker item,
  153. // immediate tells the controller to update the status right away, and should
  154. // happen ONLY when there was a successful pod run.
  155. func (c *Controller) enqueueController(obj interface{}, immediate bool) {
  156. key, err := k8scontroller.KeyFunc(obj)
  157. if err != nil {
  158. klog.Warningf("Couldn't get key for object %+v: %v", obj, err)
  159. return
  160. }
  161. backoff := time.Duration(0)
  162. if !immediate {
  163. backoff = runtime.GetBackoff(c.queue, key)
  164. }
  165. c.queue.AddAfter(key, backoff)
  166. }
  167. // worker runs a worker thread that just dequeues items, processes them, and marks them done.
  168. // It enforces that the sync is never invoked concurrently with the same key.
  169. func (c *Controller) worker() {
  170. for c.processNextWorkItem() {
  171. }
  172. }
  173. func (c *Controller) processNextWorkItem() bool {
  174. key, quit := c.queue.Get()
  175. if quit {
  176. return false
  177. }
  178. defer c.queue.Done(key)
  179. forget, err := c.sync(key.(string))
  180. if err == nil {
  181. if forget {
  182. c.queue.Forget(key)
  183. }
  184. return true
  185. }
  186. klog.Warningf("Error syncing jointinference service: %v", err)
  187. c.queue.AddRateLimited(key)
  188. return true
  189. }
  190. // sync will sync the jointinferenceservice with the given key.
  191. // This function is not meant to be invoked concurrently with the same key.
  192. func (c *Controller) sync(key string) (bool, error) {
  193. startTime := time.Now()
  194. defer func() {
  195. klog.V(4).Infof("Finished syncing jointinference service %q (%v)", key, time.Since(startTime))
  196. }()
  197. ns, name, err := cache.SplitMetaNamespaceKey(key)
  198. if err != nil {
  199. return false, err
  200. }
  201. if len(ns) == 0 || len(name) == 0 {
  202. return false, fmt.Errorf("invalid jointinference service key %q: either namespace or name is missing", key)
  203. }
  204. sharedJointinferenceservice, err := c.serviceLister.JointInferenceServices(ns).Get(name)
  205. if err != nil {
  206. if errors.IsNotFound(err) {
  207. klog.V(4).Infof("JointInferenceService has been deleted: %v", key)
  208. return true, nil
  209. }
  210. return false, err
  211. }
  212. jointinferenceservice := *sharedJointinferenceservice
  213. // if jointinferenceservice was finished previously, we don't want to redo the termination
  214. if isJointinferenceserviceFinished(&jointinferenceservice) {
  215. return true, nil
  216. }
  217. // set kind for jointinferenceservice in case that the kind is None
  218. // more details at https://github.com/kubernetes/kubernetes/issues/3030
  219. jointinferenceservice.SetGroupVersionKind(Kind)
  220. selector, _ := runtime.GenerateSelector(&jointinferenceservice)
  221. pods, err := c.podStore.Pods(jointinferenceservice.Namespace).List(selector)
  222. if err != nil {
  223. return false, err
  224. }
  225. klog.V(4).Infof("list jointinference service %v/%v, %v pods: %v", jointinferenceservice.Namespace, jointinferenceservice.Name, len(pods), pods)
  226. latestConditionLen := len(jointinferenceservice.Status.Conditions)
  227. active := runtime.CalcActivePodCount(pods)
  228. var failed int32 = 0
  229. // neededCounts means that two pods should be created successfully in a jointinference service currently
  230. // two pods consist of edge pod and cloud pod
  231. var neededCounts int32 = 2
  232. // jointinferenceservice first start
  233. if jointinferenceservice.Status.StartTime == nil {
  234. now := metav1.Now()
  235. jointinferenceservice.Status.StartTime = &now
  236. } else {
  237. failed = neededCounts - active
  238. }
  239. var manageServiceErr error
  240. serviceFailed := false
  241. var latestConditionType sednav1.JointInferenceServiceConditionType = ""
  242. // get the latest condition type
  243. // based on that condition updated is appended, not inserted.
  244. jobConditions := jointinferenceservice.Status.Conditions
  245. if len(jobConditions) > 0 {
  246. latestConditionType = (jobConditions)[len(jobConditions)-1].Type
  247. }
  248. var newCondtionType sednav1.JointInferenceServiceConditionType
  249. var reason string
  250. var message string
  251. if failed > 0 {
  252. serviceFailed = true
  253. // TODO: get the failed worker, and knows that which worker fails, edge inference worker or cloud inference worker
  254. reason = "workerFailed"
  255. message = "the worker of Jointinferenceservice failed"
  256. newCondtionType = sednav1.JointInferenceServiceCondFailed
  257. c.recorder.Event(&jointinferenceservice, v1.EventTypeWarning, reason, message)
  258. } else {
  259. if len(pods) == 0 {
  260. active, manageServiceErr = c.createWorkers(&jointinferenceservice)
  261. }
  262. if manageServiceErr != nil {
  263. serviceFailed = true
  264. message = error.Error(manageServiceErr)
  265. newCondtionType = sednav1.JointInferenceServiceCondFailed
  266. failed = neededCounts - active
  267. } else {
  268. // TODO: handle the case that the pod phase is PodSucceeded
  269. newCondtionType = sednav1.JointInferenceServiceCondRunning
  270. }
  271. }
  272. //
  273. if newCondtionType != latestConditionType {
  274. jointinferenceservice.Status.Conditions = append(jointinferenceservice.Status.Conditions, NewJointInferenceServiceCondition(newCondtionType, reason, message))
  275. }
  276. forget := false
  277. // no need to update the jointinferenceservice if the status hasn't changed since last time
  278. if jointinferenceservice.Status.Active != active || jointinferenceservice.Status.Failed != failed || len(jointinferenceservice.Status.Conditions) != latestConditionLen {
  279. jointinferenceservice.Status.Active = active
  280. jointinferenceservice.Status.Failed = failed
  281. if err := c.updateStatus(&jointinferenceservice); err != nil {
  282. return forget, err
  283. }
  284. if serviceFailed && !isJointinferenceserviceFinished(&jointinferenceservice) {
  285. // returning an error will re-enqueue jointinferenceservice after the backoff period
  286. return forget, fmt.Errorf("failed pod(s) detected for jointinference service key %q", key)
  287. }
  288. forget = true
  289. }
  290. return forget, manageServiceErr
  291. }
  292. // NewJointInferenceServiceCondition creates a new joint condition
  293. func NewJointInferenceServiceCondition(conditionType sednav1.JointInferenceServiceConditionType, reason, message string) sednav1.JointInferenceServiceCondition {
  294. return sednav1.JointInferenceServiceCondition{
  295. Type: conditionType,
  296. Status: v1.ConditionTrue,
  297. LastHeartbeatTime: metav1.Now(),
  298. LastTransitionTime: metav1.Now(),
  299. Reason: reason,
  300. Message: message,
  301. }
  302. }
  303. func (c *Controller) updateStatus(jointinferenceservice *sednav1.JointInferenceService) error {
  304. serviceClient := c.client.JointInferenceServices(jointinferenceservice.Namespace)
  305. var err error
  306. for i := 0; i <= runtime.ResourceUpdateRetries; i = i + 1 {
  307. var newJointinferenceservice *sednav1.JointInferenceService
  308. newJointinferenceservice, err = serviceClient.Get(context.TODO(), jointinferenceservice.Name, metav1.GetOptions{})
  309. if err != nil {
  310. break
  311. }
  312. newJointinferenceservice.Status = jointinferenceservice.Status
  313. if _, err = serviceClient.UpdateStatus(context.TODO(), newJointinferenceservice, metav1.UpdateOptions{}); err == nil {
  314. break
  315. }
  316. }
  317. return nil
  318. }
  319. func isJointinferenceserviceFinished(j *sednav1.JointInferenceService) bool {
  320. for _, c := range j.Status.Conditions {
  321. if (c.Type == sednav1.JointInferenceServiceCondFailed) && c.Status == v1.ConditionTrue {
  322. return true
  323. }
  324. }
  325. return false
  326. }
  327. func (c *Controller) createWorkers(service *sednav1.JointInferenceService) (active int32, err error) {
  328. active = 0
  329. // create cloud worker
  330. err = c.createCloudWorker(service)
  331. if err != nil {
  332. return active, err
  333. }
  334. active++
  335. // create k8s service for cloudPod
  336. // FIXME(llhuii): only the case that Spec.NodeName specified is support,
  337. // will support Spec.NodeSelector.
  338. bigModelIP, err := runtime.GetNodeIPByName(c.kubeClient, service.Spec.CloudWorker.Template.Spec.NodeName)
  339. bigServicePort, err := runtime.CreateKubernetesService(c.kubeClient, service, jointInferenceForCloud, bigModelPort, bigModelIP)
  340. if err != nil {
  341. return active, err
  342. }
  343. // create edge worker
  344. err = c.createEdgeWorker(service, bigServicePort)
  345. if err != nil {
  346. return active, err
  347. }
  348. active++
  349. return active, err
  350. }
  351. func (c *Controller) createCloudWorker(service *sednav1.JointInferenceService) error {
  352. // deliver pod for cloudworker
  353. cloudModelName := service.Spec.CloudWorker.Model.Name
  354. cloudModel, err := c.client.Models(service.Namespace).Get(context.Background(), cloudModelName, metav1.GetOptions{})
  355. if err != nil {
  356. return fmt.Errorf("failed to get cloud model %s: %w",
  357. cloudModelName, err)
  358. }
  359. var workerParam runtime.WorkerParam
  360. secretName := cloudModel.Spec.CredentialName
  361. var modelSecret *v1.Secret
  362. if secretName != "" {
  363. modelSecret, _ = c.kubeClient.CoreV1().Secrets(service.Namespace).Get(context.TODO(), secretName, metav1.GetOptions{})
  364. }
  365. workerParam.Mounts = append(workerParam.Mounts, runtime.WorkerMount{
  366. URL: &runtime.MountURL{
  367. URL: cloudModel.Spec.URL,
  368. Secret: modelSecret,
  369. DownloadByInitializer: true,
  370. },
  371. Name: "model",
  372. EnvName: "MODEL_URL",
  373. })
  374. workerParam.Env = map[string]string{
  375. "NAMESPACE": service.Namespace,
  376. "SERVICE_NAME": service.Name,
  377. "WORKER_NAME": "cloudworker-" + utilrand.String(5),
  378. "BIG_MODEL_BIND_PORT": strconv.Itoa(int(bigModelPort)),
  379. }
  380. workerParam.WorkerType = jointInferenceForCloud
  381. // create cloud pod
  382. _, err = runtime.CreatePodWithTemplate(c.kubeClient,
  383. service,
  384. &service.Spec.CloudWorker.Template,
  385. &workerParam)
  386. return err
  387. }
  388. func (c *Controller) createEdgeWorker(service *sednav1.JointInferenceService, bigServicePort int32) error {
  389. // deliver pod for edgeworker
  390. ctx := context.Background()
  391. edgeModelName := service.Spec.EdgeWorker.Model.Name
  392. edgeModel, err := c.client.Models(service.Namespace).Get(ctx, edgeModelName, metav1.GetOptions{})
  393. if err != nil {
  394. return fmt.Errorf("failed to get edge model %s: %w",
  395. edgeModelName, err)
  396. }
  397. secretName := edgeModel.Spec.CredentialName
  398. var modelSecret *v1.Secret
  399. if secretName != "" {
  400. modelSecret, _ = c.kubeClient.CoreV1().Secrets(service.Namespace).Get(context.TODO(), secretName, metav1.GetOptions{})
  401. }
  402. // FIXME(llhuii): only the case that Spec.NodeName specified is support,
  403. // will support Spec.NodeSelector.
  404. // get bigModelIP from nodeName in cloudWorker
  405. bigModelIP, err := runtime.GetNodeIPByName(c.kubeClient, service.Spec.CloudWorker.Template.Spec.NodeName)
  406. if err != nil {
  407. return fmt.Errorf("failed to get node ip: %w", err)
  408. }
  409. edgeWorker := service.Spec.EdgeWorker
  410. HEMParameterJSON, _ := json.Marshal(edgeWorker.HardExampleMining.Parameters)
  411. HEMParameterString := string(HEMParameterJSON)
  412. var workerParam runtime.WorkerParam
  413. workerParam.Mounts = append(workerParam.Mounts, runtime.WorkerMount{
  414. URL: &runtime.MountURL{
  415. URL: edgeModel.Spec.URL,
  416. Secret: modelSecret,
  417. DownloadByInitializer: true,
  418. },
  419. Name: "model",
  420. EnvName: "MODEL_URL",
  421. })
  422. workerParam.Env = map[string]string{
  423. "NAMESPACE": service.Namespace,
  424. "SERVICE_NAME": service.Name,
  425. "WORKER_NAME": "edgeworker-" + utilrand.String(5),
  426. "BIG_MODEL_IP": bigModelIP,
  427. "BIG_MODEL_PORT": strconv.Itoa(int(bigServicePort)),
  428. "HEM_NAME": edgeWorker.HardExampleMining.Name,
  429. "HEM_PARAMETERS": HEMParameterString,
  430. "LC_SERVER": c.cfg.LC.Server,
  431. }
  432. workerParam.WorkerType = jointInferenceForEdge
  433. workerParam.HostNetwork = true
  434. // create edge pod
  435. _, err = runtime.CreatePodWithTemplate(c.kubeClient,
  436. service,
  437. &service.Spec.EdgeWorker.Template,
  438. &workerParam)
  439. return err
  440. }
  441. // New creates a new JointInferenceService controller that keeps the relevant pods
  442. // in sync with their corresponding JointInferenceService objects.
  443. func New(cc *runtime.ControllerContext) (runtime.FeatureControllerI, error) {
  444. cfg := cc.Config
  445. podInformer := cc.KubeInformerFactory.Core().V1().Pods()
  446. serviceInformer := cc.SednaInformerFactory.Sedna().V1alpha1().JointInferenceServices()
  447. eventBroadcaster := record.NewBroadcaster()
  448. eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: cc.KubeClient.CoreV1().Events("")})
  449. jc := &Controller{
  450. kubeClient: cc.KubeClient,
  451. client: cc.SednaClient.SednaV1alpha1(),
  452. queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(runtime.DefaultBackOff, runtime.MaxBackOff), "jointinferenceservice"),
  453. recorder: eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "jointinferenceservice-controller"}),
  454. cfg: cfg,
  455. }
  456. serviceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
  457. AddFunc: func(obj interface{}) {
  458. jc.enqueueController(obj, true)
  459. },
  460. UpdateFunc: func(old, cur interface{}) {
  461. jc.enqueueController(cur, true)
  462. },
  463. DeleteFunc: func(obj interface{}) {
  464. jc.enqueueController(obj, true)
  465. },
  466. })
  467. jc.serviceLister = serviceInformer.Lister()
  468. jc.serviceStoreSynced = serviceInformer.Informer().HasSynced
  469. podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
  470. AddFunc: jc.addPod,
  471. UpdateFunc: jc.updatePod,
  472. DeleteFunc: jc.deletePod,
  473. })
  474. jc.podStore = podInformer.Lister()
  475. jc.podStoreSynced = podInformer.Informer().HasSynced
  476. jc.addUpstreamHandler(cc)
  477. return jc, nil
  478. }