Skip to content

Commit c36e7f2

Browse files
authored
serving interface support affinity & anti-affinity (#946)
* serving interface support affinity & anti-affinity Signed-off-by: MikkiYang <hecy7@asiainfo.com> * serving interface support affinity & anti-affinity Signed-off-by: MikkiYang <hecy7@asiainfo.com> * serving interface support affinity & anti-affinity Signed-off-by: MikkiYang <hecy7@asiainfo.com> * serving interface support affinity & anti-affinity Signed-off-by: MikkiYang <hecy7@asiainfo.com> * serving interface support affinity & anti-affinity Signed-off-by: MikkiYang <hecy7@asiainfo.com> * serving interface support affinity & anti-affinity Signed-off-by: MikkiYang <hecy7@asiainfo.com> * serving interface support affinity & anti-affinity Signed-off-by: MikkiYang <hecy7@asiainfo.com> * serving interface support affinity & anti-affinity Signed-off-by: MikkiYang <hecy7@asiainfo.com> * serving interface support affinity & anti-affinity Signed-off-by: MikkiYang <hecy7@asiainfo.com> --------- Signed-off-by: MikkiYang <hecy7@asiainfo.com>
1 parent 5e058da commit c36e7f2

File tree

9 files changed

+567
-248
lines changed

9 files changed

+567
-248
lines changed

docs/locales/en/LC_MESSAGES/reference/apis/serving_cn.po

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ msgstr "[ServingParty](#serving-party)[]"
218218
msgid "参与方信息"
219219
msgstr "Party Information"
220220

221+
#: ../../reference/apis/serving_cn.md
222+
msgid "Pod 调度亲和性模式。可选值:\"none\"(无亲和性)、\"affinity\"(亲和性)、\"anti-affinity\"(反亲和性)。默认值为 \"anti-affinity\"(如果未指定或为空)"
223+
msgstr "Pod scheduling affinity mode. Optional values: \"none\" (no affinity), \"affinity\" (affinity), \"anti-affinity\" (anti-affinity). The default value is \"anti-affinity\" (if not specified or empty)"
224+
221225
#: ../../reference/apis/serving_cn.md:36
222226
msgid "响应(CreateServingResponse)"
223227
msgstr "Response (CreateServingResponse)"
@@ -299,6 +303,10 @@ msgstr "Initiator Node ID"
299303
msgid "data.parties"
300304
msgstr "data.parties"
301305

306+
#: ../../reference/apis/serving_cn.md
307+
msgid "亲和性模式"
308+
msgstr "affinity mode"
309+
302310
#: ../../reference/apis/serving_cn.md
303311
msgid "data.status"
304312
msgstr "data.status"

docs/reference/apis/serving_cn.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
| serving_input_config | string | 可选 | 应用配置。[Secretflow Serving 预测应用配置参考](https://www.secretflow.org.cn/zh-CN/docs/serving/main/topics/deployment/serving_on_kuscia#configuration-description) |
3333
| initiator | string | 必填 | 发起方节点 DomainID |
3434
| parties | [ServingParty](#serving-party)[] | 必填 | 参与方信息 |
35+
| affinity_mode | string | 可选 | Pod 调度亲和性模式。可选值:"none"(无亲和性)、"affinity"(亲和性)、"anti-affinity"(反亲和性)。默认值为 "anti-affinity"(如果未指定或为空) |
3536

3637
#### 响应(CreateServingResponse)
3738

@@ -56,6 +57,7 @@ curl -k -X POST 'https://localhost:8082/api/v1/serving/create' \
5657
"serving_id": "serving-1",
5758
"serving_input_config": "{\"partyConfigs\":{\"alice\":{\"serverConfig\":{\"featureMapping\":{\"v24\":\"x24\",\"v22\":\"x22\",\"v21\":\"x21\",\"v25\":\"x25\",\"v23\":\"x23\"}},\"modelConfig\":{\"modelId\":\"glm-test-1\",\"basePath\":\"/tmp/alice\",\"sourcePath\":\"examples/alice/glm-test.tar.gz\",\"sourceType\":\"ST_FILE\"},\"featureSourceConfig\":{\"mockOpts\":{}},\"channel_desc\":{\"protocol\":\"http\"}},\"bob\":{\"serverConfig\":{\"featureMapping\":{\"v6\":\"x6\",\"v7\":\"x7\",\"v8\":\"x8\",\"v9\":\"x9\",\"v10\":\"x10\"}},\"modelConfig\":{\"modelId\":\"glm-test-1\",\"basePath\":\"/tmp/bob\",\"sourcePath\":\"examples/bob/glm-test.tar.gz\",\"sourceType\":\"ST_FILE\"},\"featureSourceConfig\":{\"mockOpts\":{}},\"channel_desc\":{\"protocol\":\"http\"}}}}",
5859
"initiator": "alice",
60+
"affinity_mode": "anti-affinity",
5961
"parties": [
6062
{
6163
"domain_id": "alice",

docs/tutorial/run_sf_serving_with_api_cn.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ Kuscia API 使用双向 HTTPS,所以需要配置您的客户端库的双向 HT
9898
"serving_id": "serving-glm-test-1",
9999
"initiator": "alice",
100100
"serving_input_config": "{\"partyConfigs\":{\"alice\":{\"serverConfig\":{\"featureMapping\":{\"v24\":\"x24\",\"v22\":\"x22\",\"v21\":\"x21\",\"v25\":\"x25\",\"v23\":\"x23\"}},\"modelConfig\":{\"modelId\":\"glm-test-1\",\"basePath\":\"/tmp/alice\",\"sourcePath\":\"/root/sf_serving/examples/alice/glm-test.tar.gz\",\"sourceType\":\"ST_FILE\"},\"featureSourceConfig\":{\"mockOpts\":{}},\"channel_desc\":{\"protocol\":\"http\"}},\"bob\":{\"serverConfig\":{\"featureMapping\":{\"v6\":\"x6\",\"v7\":\"x7\",\"v8\":\"x8\",\"v9\":\"x9\",\"v10\":\"x10\"}},\"modelConfig\":{\"modelId\":\"glm-test-1\",\"basePath\":\"/tmp/bob\",\"sourcePath\":\"/root/sf_serving/examples/bob/glm-test.tar.gz\",\"sourceType\":\"ST_FILE\"},\"featureSourceConfig\":{\"mockOpts\":{}},\"channel_desc\":{\"protocol\":\"http\"}}}}",
101+
"affinity_mode": "anti-affinity",
101102
"parties": [{
102103
"app_image": "secretflow-serving-image",
103104
"domain_id": "alice"

pkg/common/constants.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,14 @@ const (
7474
LabelPodUID = "kuscia.secretflow/pod-uid"
7575
LabelOwnerReferences = "kuscia.secretflow/owner-references"
7676

77-
LabelDomainRoutePartner = "kuscia.secertflow/domainroute-partner"
77+
LabelDomainRoutePartner = "kuscia.secertflow/domainroute-partner"
78+
AffinityModeAnnotationKey = "kuscia.secretflow/affinity-mode"
79+
)
80+
81+
const (
82+
AffinityModeNone = "none"
83+
AffinityModeAffinity = "affinity"
84+
AffinityModeAntiAffinity = "anti-affinity"
7885
)
7986

8087
const (

pkg/controllers/kusciadeployment/reconcile.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -644,8 +644,10 @@ func (c *Controller) generateDeployment(partyKitInfo *PartyKitInfo) (*appsv1.Dep
644644
affinity = partyKitInfo.deployTemplate.Spec.Affinity.DeepCopy()
645645
buildAffinity(affinity, partyKitInfo.dkInfo.deploymentName)
646646
} else {
647-
// If affinity is not set in AppImage, add the default PodAntiAffinity
648-
affinity = buildDefaultPodAntiAffinity(partyKitInfo.dkInfo.deploymentName)
647+
affinityMode := partyKitInfo.kd.Annotations[common.AffinityModeAnnotationKey]
648+
if affinityMode != common.AffinityModeNone {
649+
affinity = buildDefaultPodAntiAffinity(partyKitInfo.dkInfo.deploymentName)
650+
}
649651
}
650652

651653
automountServiceAccountToken := false
@@ -870,10 +872,12 @@ func (c *Controller) updateDeployment(ctx context.Context, partyKitInfo *PartyKi
870872
deploymentCopy.Spec.Template.Spec.Affinity = affinity
871873
}
872874
} else {
873-
// If affinity is not set in AppImage and original deployment Affinity is nil, add the default PodAntiAffinity
874875
if deploymentCopy.Spec.Template.Spec.Affinity == nil {
875-
needUpdate = true
876-
deploymentCopy.Spec.Template.Spec.Affinity = buildDefaultPodAntiAffinity(deploymentCopy.Name)
876+
affinityMode := partyKitInfo.kd.Annotations[common.AffinityModeAnnotationKey]
877+
if affinityMode != common.AffinityModeNone {
878+
needUpdate = true
879+
deploymentCopy.Spec.Template.Spec.Affinity = buildDefaultPodAntiAffinity(deploymentCopy.Name)
880+
}
877881
}
878882
}
879883

pkg/kusciaapi/service/serving_service.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,14 @@ func (s *servingService) buildKusciaDeployment(ctx context.Context, request *kus
226226
if len(containers) > 0 {
227227
kdParties[i].Template.Spec.Containers = containers
228228
}
229+
230+
// Get affinity mode with default value handling
231+
affinityMode := s.getAffinityMode(request.AffinityMode)
232+
// Set affinity based on affinity mode
233+
affinity := s.buildAffinityForMode(affinityMode, request.ServingId)
234+
if affinity != nil {
235+
kdParties[i].Template.Spec.Affinity = affinity
236+
}
229237
}
230238

231239
kd := &v1alpha1.KusciaDeployment{
@@ -235,6 +243,9 @@ func (s *servingService) buildKusciaDeployment(ctx context.Context, request *kus
235243
Labels: map[string]string{
236244
common.LabelKusciaDeploymentAppType: string(common.ServingApp),
237245
},
246+
Annotations: map[string]string{
247+
common.AffinityModeAnnotationKey: request.AffinityMode,
248+
},
238249
},
239250
Spec: v1alpha1.KusciaDeploymentSpec{
240251
Initiator: request.Initiator,
@@ -1058,3 +1069,67 @@ func getServingState(phase v1alpha1.KusciaDeploymentPhase) string {
10581069
return kusciaapi.ServingState_Unknown.String()
10591070
}
10601071
}
1072+
1073+
// getAffinityMode returns the affinity mode with default value handling.
1074+
// If the mode is empty or nil, it defaults to "anti-affinity".
1075+
func (s *servingService) getAffinityMode(mode string) string {
1076+
if mode == "" {
1077+
return common.AffinityModeAntiAffinity
1078+
}
1079+
return mode
1080+
}
1081+
1082+
// buildAffinityForMode builds affinity based on the specified mode.
1083+
// Returns nil for "none" mode, PodAffinity for "affinity" mode, and PodAntiAffinity for "anti-affinity" mode.
1084+
// All affinities use PreferredDuringSchedulingIgnoredDuringExecution (soft preference).
1085+
func (s *servingService) buildAffinityForMode(mode, servingID string) *corev1.Affinity {
1086+
switch mode {
1087+
case common.AffinityModeNone:
1088+
return nil
1089+
case common.AffinityModeAffinity:
1090+
return s.buildPodAffinity(servingID)
1091+
case common.AffinityModeAntiAffinity:
1092+
return s.buildPodAntiAffinity(servingID)
1093+
default:
1094+
// Default to anti-affinity for unknown modes
1095+
return s.buildPodAntiAffinity(servingID)
1096+
}
1097+
}
1098+
1099+
// buildPodAffinity builds a PodAffinity using PreferredDuringSchedulingIgnoredDuringExecution.
1100+
// Uses default weight value of 100
1101+
func (s *servingService) buildPodAffinity(servingID string) *corev1.Affinity {
1102+
return &corev1.Affinity{
1103+
PodAffinity: &corev1.PodAffinity{
1104+
PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{
1105+
s.buildWeightedPodAffinityTerm(servingID),
1106+
},
1107+
},
1108+
}
1109+
}
1110+
1111+
// buildPodAntiAffinity builds a PodAntiAffinity using PreferredDuringSchedulingIgnoredDuringExecution.
1112+
// Uses default weight value of 100
1113+
func (s *servingService) buildPodAntiAffinity(servingID string) *corev1.Affinity {
1114+
return &corev1.Affinity{
1115+
PodAntiAffinity: &corev1.PodAntiAffinity{
1116+
PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{
1117+
s.buildWeightedPodAffinityTerm(servingID),
1118+
},
1119+
},
1120+
}
1121+
}
1122+
1123+
func (s *servingService) buildWeightedPodAffinityTerm(servingID string) corev1.WeightedPodAffinityTerm {
1124+
return corev1.WeightedPodAffinityTerm{
1125+
Weight: 100,
1126+
PodAffinityTerm: corev1.PodAffinityTerm{
1127+
LabelSelector: &metav1.LabelSelector{
1128+
MatchLabels: map[string]string{
1129+
common.LabelKusciaDeploymentName: servingID,
1130+
},
1131+
},
1132+
TopologyKey: "kubernetes.io/hostname",
1133+
},
1134+
}
1135+
}

pkg/kusciaapi/service/serving_service_test.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,3 +944,210 @@ func TestUpdateKusciaDeploymentParty(t *testing.T) {
944944
})
945945
}
946946
}
947+
948+
func TestGetAffinityMode(t *testing.T) {
949+
s := servingService{}
950+
tests := []struct {
951+
name string
952+
mode string
953+
expected string
954+
}{
955+
{
956+
name: "empty mode should default to anti-affinity",
957+
mode: "",
958+
expected: "anti-affinity",
959+
},
960+
{
961+
name: "none mode",
962+
mode: "none",
963+
expected: "none",
964+
},
965+
{
966+
name: "affinity mode",
967+
mode: "affinity",
968+
expected: "affinity",
969+
},
970+
{
971+
name: "anti-affinity mode",
972+
mode: "anti-affinity",
973+
expected: "anti-affinity",
974+
},
975+
}
976+
977+
for _, tt := range tests {
978+
t.Run(tt.name, func(t *testing.T) {
979+
got := s.getAffinityMode(tt.mode)
980+
assert.Equal(t, tt.expected, got)
981+
})
982+
}
983+
}
984+
985+
func TestBuildAffinityForMode(t *testing.T) {
986+
s := servingService{}
987+
servingID := "test-serving"
988+
989+
tests := []struct {
990+
name string
991+
mode string
992+
expected *corev1.Affinity
993+
}{
994+
{
995+
name: "none mode should return nil",
996+
mode: "none",
997+
expected: nil,
998+
},
999+
{
1000+
name: "affinity mode should return PodAffinity",
1001+
mode: "affinity",
1002+
expected: &corev1.Affinity{
1003+
PodAffinity: &corev1.PodAffinity{
1004+
PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{
1005+
{
1006+
Weight: 100,
1007+
PodAffinityTerm: corev1.PodAffinityTerm{
1008+
LabelSelector: &metav1.LabelSelector{
1009+
MatchLabels: map[string]string{
1010+
"kuscia.secretflow/kd-name": servingID,
1011+
},
1012+
},
1013+
TopologyKey: "kubernetes.io/hostname",
1014+
},
1015+
},
1016+
},
1017+
},
1018+
},
1019+
},
1020+
{
1021+
name: "anti-affinity mode should return PodAntiAffinity",
1022+
mode: "anti-affinity",
1023+
expected: &corev1.Affinity{
1024+
PodAntiAffinity: &corev1.PodAntiAffinity{
1025+
PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{
1026+
{
1027+
Weight: 100,
1028+
PodAffinityTerm: corev1.PodAffinityTerm{
1029+
LabelSelector: &metav1.LabelSelector{
1030+
MatchLabels: map[string]string{
1031+
"kuscia.secretflow/kd-name": servingID,
1032+
},
1033+
},
1034+
TopologyKey: "kubernetes.io/hostname",
1035+
},
1036+
},
1037+
},
1038+
},
1039+
},
1040+
},
1041+
{
1042+
name: "unknown mode should default to anti-affinity",
1043+
mode: "unknown",
1044+
expected: &corev1.Affinity{
1045+
PodAntiAffinity: &corev1.PodAntiAffinity{
1046+
PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{
1047+
{
1048+
Weight: 100,
1049+
PodAffinityTerm: corev1.PodAffinityTerm{
1050+
LabelSelector: &metav1.LabelSelector{
1051+
MatchLabels: map[string]string{
1052+
"kuscia.secretflow/kd-name": servingID,
1053+
},
1054+
},
1055+
TopologyKey: "kubernetes.io/hostname",
1056+
},
1057+
},
1058+
},
1059+
},
1060+
},
1061+
},
1062+
}
1063+
1064+
for _, tt := range tests {
1065+
t.Run(tt.name, func(t *testing.T) {
1066+
got := s.buildAffinityForMode(tt.mode, servingID)
1067+
if tt.expected == nil {
1068+
assert.Nil(t, got)
1069+
} else {
1070+
assert.NotNil(t, got)
1071+
if tt.mode == "affinity" {
1072+
assert.NotNil(t, got.PodAffinity)
1073+
assert.Nil(t, got.PodAntiAffinity)
1074+
assert.Equal(t, tt.expected.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Weight, got.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Weight)
1075+
} else {
1076+
assert.NotNil(t, got.PodAntiAffinity)
1077+
assert.Nil(t, got.PodAffinity)
1078+
assert.Equal(t, tt.expected.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Weight, got.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Weight)
1079+
}
1080+
}
1081+
})
1082+
}
1083+
}
1084+
1085+
func TestBuildKusciaDeploymentWithAffinityMode(t *testing.T) {
1086+
replicas := int32(2)
1087+
parties := []*kusciaapi.ServingParty{{
1088+
DomainId: "alice",
1089+
AppImage: "mockImageName",
1090+
Replicas: &replicas,
1091+
}}
1092+
1093+
kusciaClient := kusciafake.NewSimpleClientset(makeMockAppImage("mockImageName"))
1094+
s := servingService{kusciaClient: kusciaClient}
1095+
1096+
tests := []struct {
1097+
name string
1098+
affinityMode string
1099+
expectAffinity bool
1100+
}{
1101+
{
1102+
name: "affinity_mode is none, should not set affinity in party template",
1103+
affinityMode: "none",
1104+
expectAffinity: false,
1105+
},
1106+
{
1107+
name: "affinity_mode is affinity, should set PodAffinity",
1108+
affinityMode: "affinity",
1109+
expectAffinity: true,
1110+
},
1111+
{
1112+
name: "affinity_mode is anti-affinity, should set PodAntiAffinity",
1113+
affinityMode: "anti-affinity",
1114+
expectAffinity: true,
1115+
},
1116+
{
1117+
name: "affinity_mode is empty, should default to anti-affinity",
1118+
affinityMode: "",
1119+
expectAffinity: true,
1120+
},
1121+
}
1122+
1123+
for _, tt := range tests {
1124+
t.Run(tt.name, func(t *testing.T) {
1125+
req := &kusciaapi.CreateServingRequest{
1126+
ServingId: "test-serving",
1127+
Initiator: "alice",
1128+
AffinityMode: tt.affinityMode,
1129+
Parties: parties,
1130+
}
1131+
1132+
kd, err := s.buildKusciaDeployment(context.Background(), req)
1133+
assert.NoError(t, err)
1134+
assert.NotNil(t, kd)
1135+
1136+
if tt.expectAffinity {
1137+
assert.NotNil(t, kd.Spec.Parties[0].Template.Spec.Affinity)
1138+
if tt.affinityMode == "affinity" || tt.affinityMode == "" {
1139+
// Empty mode defaults to anti-affinity
1140+
if tt.affinityMode == "affinity" {
1141+
assert.NotNil(t, kd.Spec.Parties[0].Template.Spec.Affinity.PodAffinity)
1142+
assert.Nil(t, kd.Spec.Parties[0].Template.Spec.Affinity.PodAntiAffinity)
1143+
} else {
1144+
assert.NotNil(t, kd.Spec.Parties[0].Template.Spec.Affinity.PodAntiAffinity)
1145+
}
1146+
}
1147+
} else {
1148+
// When affinity_mode is "none", affinity should be nil
1149+
assert.Nil(t, kd.Spec.Parties[0].Template.Spec.Affinity)
1150+
}
1151+
})
1152+
}
1153+
}

0 commit comments

Comments
 (0)