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

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