Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions llm/android/LlamaDemo/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,14 @@ dependencies {
if (useLocalAar == true) {
implementation(files("libs/executorch.aar"))
} else {
implementation("org.pytorch:executorch-android:1.0.1")
implementation("org.pytorch:executorch-android:1.1.0")
// https://mvnrepository.com/artifact/org.pytorch/executorch-android-qnn
// Uncomment this to enable QNN
// implementation("org.pytorch:executorch-android-qnn:1.0.1")
// implementation("org.pytorch:executorch-android-qnn:1.1.0")

// https://mvnrepository.com/artifact/org.pytorch/executorch-android-vulkan
// uncomment to enable vulkan
// implementation("org.pytorch:executorch-android-vulkan:1.0.1")
// implementation("org.pytorch:executorch-android-vulkan:1.1.0")
}
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.activity:activity:1.9.0")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.example.executorchllamademo

/**
* Represents a single PTE model configuration.
* Multiple ModelConfigurations can share the same PTD (data path) file for LoRA support.
*/
data class ModelConfiguration(
val id: String = "",
val modelFilePath: String = "",
val tokenizerFilePath: String = "",
val modelType: ModelType = ModelType.LLAMA_3,
val backendType: BackendType = BackendType.XNNPACK,
val temperature: Double = ModuleSettings.DEFAULT_TEMPERATURE,
val displayName: String = "",
val adapterFilePaths: List<String> = emptyList()
) {
companion object {
fun create(
modelFilePath: String,
tokenizerFilePath: String,
modelType: ModelType,
backendType: BackendType,
temperature: Double
): ModelConfiguration {
return ModelConfiguration(
id = generateId(modelFilePath),
modelFilePath = modelFilePath,
tokenizerFilePath = tokenizerFilePath,
modelType = modelType,
backendType = backendType,
temperature = temperature,
displayName = extractDisplayName(modelFilePath)
)
}

private fun generateId(modelFilePath: String): String {
return modelFilePath.hashCode().toString()
}

private fun extractDisplayName(filePath: String): String {
if (filePath.isEmpty()) return ""
return filePath.substringAfterLast('/')
}
}

fun isValid(): Boolean {
return modelFilePath.isNotEmpty() && tokenizerFilePath.isNotEmpty()
}

fun withModelFilePath(path: String): ModelConfiguration {
return copy(
modelFilePath = path,
id = generateId(path),
displayName = extractDisplayName(path)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ package com.example.executorchllamademo

/**
* Holds module-specific settings for the current model/tokenizer configuration.
* Supports both legacy single-model and multi-model configurations for LoRA support.
*/
data class ModuleSettings(
// Legacy single-model fields (kept for backward compatibility)
val modelFilePath: String = "",
val tokenizerFilePath: String = "",
val dataPath: String = "",
Expand All @@ -21,22 +23,135 @@ data class ModuleSettings(
val modelType: ModelType = DEFAULT_MODEL,
val backendType: BackendType = DEFAULT_BACKEND,
val isClearChatHistory: Boolean = false,
val isLoadModel: Boolean = false
val isLoadModel: Boolean = false,

// LoRA mode toggle - when enabled, allows multiple model selection
val isLoraMode: Boolean = false,

// Foundation PTD path - shared base weights for all LoRA models
val foundationDataPath: String = "",

// Multi-model support fields (used when isLoraMode is true)
val models: List<ModelConfiguration> = emptyList(),
val activeModelId: String = "",
val sharedDataPath: String = "",
val foundationModelType: ModelType = ModelType.LLAMA_3
) {
/**
* Gets the effective model type, considering multi-model configuration.
*/
fun getEffectiveModelType(): ModelType {
val activeModel = getActiveModel()
return activeModel?.modelType ?: modelType
}

fun getFormattedSystemPrompt(): String {
return PromptFormat.getSystemPromptTemplate(modelType)
return PromptFormat.getSystemPromptTemplate(getEffectiveModelType())
.replace(PromptFormat.SYSTEM_PLACEHOLDER, systemPrompt)
}

fun getFormattedUserPrompt(prompt: String, thinkingMode: Boolean): String {
val effectiveType = getEffectiveModelType()
return userPrompt
.replace(PromptFormat.USER_PLACEHOLDER, prompt)
.replace(
PromptFormat.THINKING_MODE_PLACEHOLDER,
PromptFormat.getThinkingModeToken(modelType, thinkingMode)
PromptFormat.getThinkingModeToken(effectiveType, thinkingMode)
)
}

/**
* Gets the active model configuration if using multi-model mode.
*/
fun getActiveModel(): ModelConfiguration? {
if (models.isEmpty() || activeModelId.isEmpty()) return null
return models.find { it.id == activeModelId }
}

/**
* Gets a model configuration by ID.
*/
fun getModelById(modelId: String): ModelConfiguration? {
return models.find { it.id == modelId }
}

/**
* Gets the effective foundation data path for LoRA mode.
* Priority: foundationDataPath > sharedDataPath > dataPath
*/
fun getEffectiveDataPath(): String {
return foundationDataPath.ifEmpty { sharedDataPath.ifEmpty { dataPath } }
}

/**
* Checks if there are multiple models configured.
*/
fun hasMultipleModels(): Boolean = models.size > 1

/**
* Checks if any models are configured.
*/
fun hasModels(): Boolean = models.isNotEmpty()

/**
* Adds a model to the list. If a model with the same ID exists, it's replaced.
* If this is the first model, it becomes the active model.
*/
fun addModel(model: ModelConfiguration): ModuleSettings {
val existingIndex = models.indexOfFirst { it.id == model.id }
val newModels = if (existingIndex >= 0) {
models.toMutableList().apply { this[existingIndex] = model }
} else {
models + model
}
val newActiveId = if (models.isEmpty()) model.id else activeModelId
return copy(models = newModels, activeModelId = newActiveId)
}

/**
* Removes a model by ID. If the active model is removed, selects another.
*/
fun removeModel(modelId: String): ModuleSettings {
val newModels = models.filter { it.id != modelId }
val newActiveId = if (activeModelId == modelId) {
newModels.firstOrNull()?.id ?: ""
} else {
activeModelId
}
return copy(models = newModels, activeModelId = newActiveId)
}

/**
* Sets the active model by ID.
*/
fun setActiveModel(modelId: String): ModuleSettings {
return copy(activeModelId = modelId)
}

/**
* Migrates legacy single-model settings to multi-model format.
*/
fun migrateToMultiModel(): ModuleSettings {
if (models.isNotEmpty()) return this

// Only migrate if there's a valid legacy model configuration
if (modelFilePath.isEmpty() || tokenizerFilePath.isEmpty()) return this

val legacyModel = ModelConfiguration.create(
modelFilePath = modelFilePath,
tokenizerFilePath = tokenizerFilePath,
modelType = modelType,
backendType = backendType,
temperature = temperature
)

return copy(
models = listOf(legacyModel),
activeModelId = legacyModel.id,
sharedDataPath = dataPath
)
}

companion object {
const val DEFAULT_TEMPERATURE = 0.0
val DEFAULT_MODEL = ModelType.LLAMA_3
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.example.executorchllamademo.ui.components

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.RadioButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.executorchllamademo.ModelConfiguration
import com.example.executorchllamademo.ui.theme.LocalAppColors

/**
* A composable that displays a single model configuration in a list.
* Shows the model name, type, backend, and tokenizer, with a radio button
* for selection and a remove button.
*/
@Composable
fun ModelListItem(
model: ModelConfiguration,
isActive: Boolean,
onSelect: () -> Unit,
onRemove: () -> Unit,
modifier: Modifier = Modifier
) {
val appColors = LocalAppColors.current

Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onSelect)
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = isActive,
onClick = onSelect,
colors = RadioButtonDefaults.colors(
selectedColor = appColors.settingsText,
unselectedColor = appColors.settingsSecondaryText
)
)

Spacer(modifier = Modifier.width(8.dp))

Column(
modifier = Modifier.weight(1f)
) {
// Model name (display name from file path)
Text(
text = model.displayName.ifEmpty { "Unknown Model" },
color = appColors.settingsText,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)

// Tokenizer name
val tokenizerName = model.tokenizerFilePath.substringAfterLast('/').ifEmpty { "No tokenizer" }
Text(
text = tokenizerName,
color = appColors.settingsSecondaryText.copy(alpha = 0.7f),
fontSize = 11.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}

IconButton(
onClick = onRemove,
modifier = Modifier.size(40.dp)
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Remove model",
tint = Color(0xFFFF6666)
)
}
}
}
Loading