跳至主要内容

Gemma 3 270M:含演示项目的指南(Android 应用)

了解如何在 Android 应用中设置、集成并运行 Gemma 3 270M 模型,以及确保其在设备端推理可靠与安全的最佳实践。
· 12分钟 阅读

使用 AI 探索

Google 最近发布了 Gemma 3 270M,这是一款拥有 2.7 亿参数的超紧凑模型,代表了高效 AI 部署的新范式。不同于大型通用模型,Gemma 3 270M 专为任务定制微调而设计,同时开箱即具备指令跟随能力。

在本教程中,我将带您入门 Gemma 3 270M,并演示为何它非常适合需要以下能力的开发者:

  • 推理极快,计算开销极小
  • 极致能效(在 Pixel 9 Pro 上进行 25 次对话仅耗电 0.75%)
  • 支持 INT4 量化,性能损失极小
  • 完全在设备端执行,隐私有保障

我们通过每周五的免费通讯 The Median 为读者解读最新 AI 动态,一周只需几分钟即可掌握关键信息。订阅以保持敏锐:

什么是 Gemma 3 270M?

Gemma 3 270M 是一款紧凑的基础模型,仅用 2.7 亿参数即可实现强大的指令跟随与结构化文本生成。它是 Gemma 3 家族中最小的成员,但继承了与 Gemma 3 1B 与 4B 等更大变体相同的架构与训练策略。

不同于通用大型模型,Gemma 3 270M 专注于高效与专用化。以下是该模型的一些要点:

  • 在移动设备上可轻松运行,量化推理(INT4、Q8 等)在多轮对话中耗电低于 1%。
  • 包含 1 亿个 Transformer 参数与 1.7 亿的嵌入矩阵,支持 256,000 个词表,对处理特定领域的罕见词非常理想。
  • 提供指令微调后的检查点,便于构建无需云连接的助手、数据抽取器与创作工具。

Gemma 3 270M performance on IFEval

Gemma 3 270M 具备良好的指令跟随能力——IFEval 基准用于测试模型遵循可验证信息的能力。来源:Google

凭借 32K 上下文窗口、低内存占用,以及在实体抽取、分类与轻量合规检查等任务上的稳健准确率,Gemma 3 270M 非常适合追求隐私、速度与成本效益的边缘 AI开发者。

如何获取 Gemma 3 270M?

根据您偏好的平台,有多种简单方式可以开始使用 Gemma 3 270M:

1. Cloud Run

您可以在 Google Cloud Run 上使用预构建的 GPU 加速容器来部署 Gemma 3,以实现可扩展推理。对于 Gemma 3 270M,您需要基于 Gemma-on-CloudRun GitHub 仓库自行构建容器,因为仅有更大变体(1B+)提供预构建版本。请按照 Google Cloud Run 的官方指南,为 Web 或后端应用启用快速、安全、具成本效益的推理。

2. Android(推荐)

最佳的用户体验是在设备端,使用 Gemma 3 270M 的量化版本(gemma3-270m-it-q8)以及 Google 提供的官方 Gemma Gallery App。这种方式带来极快的离线推理、强隐私保护以及极低的电量消耗。

3. 本地推理

您也可以在笔记本电脑上通过以下工具运行 Gemma 3 270M:

  • LM Studio:在 LM Studio 环境中下载扩展名为 .gguf 或 .litertlm 的模型,立即在其聊天界面中运行或本地托管。
  • Ollama:您也可以使用 ollama run gemma3:270m 命令在本机本地运行该模型。
  • Llama.cpp:同样提供 Gemma 3 270M,用于超轻量的 CPU 推理。
  • Docker:您还可以通过 Docker 容器运行 Gemma 3,可从 Hugging Face 或 Gemma 官方 GitHub 仓库获取。
  • HuggingFace:用户可直接在 HuggingFace 界面访问或推理该模型,或下载预训练与指令微调版本以便本地使用。

4. 微调模型

Gemma 3 270M 也可通过 Hugging Face Transformers、JAX 或 Unsloth 在您的领域数据上进行微调。模型体量小,适合快速、低成本的实验;QAT 检查点支持在微调后直接部署 INT4。

接下来,我将带您构建一个可用的 Android 应用,使用 Gemma 3 270M 处理文本提示。我们将以 Google 的官方 Gallery 仓库为基础进行自定义。

步骤 1:克隆并设置应用

首先新建项目并克隆 Google 的原始仓库。

步骤 1.1:创建新的 Android Studio 项目

在您的电脑上,打开 Android Studio,以新项目打开,并选择 Empty Activity。

Android Studio New Project window for running gemma 3 270M

步骤 1.2:设置新的空 Activity

接着,填写您的 Activity 名称(如“Gemma 3 270M”),其余保持默认。然后点击 Finish。

步骤 1.3:克隆主仓库

现在,在 Android Studio 内打开终端(左下角),执行以下 bash 命令:

git clone https://github.com/google-ai-edge/gallery
cd gallery/android

Android Studio Terminal for running gemma 3 270M

这将打开您的项目。您可以在左侧标签查看所有项目文件。

Android Studio Files

步骤 2:连接模型

要将 Gemma 3 270M 模型连接到您的 Android 应用,您需要在应用的 Tasks.kt 文件中将其注册为新任务。

进入 data/ 文件夹并打开 Tasks.kt。在那里,您将为模型的 Q8 量化变体 gemma3-270m-it-q8 专门定义一个新任务条目,该变体针对低内存与设备端推理进行了优化。该格式适合 RAM 有限的移动设备。

Tasks.kt file location

注意:本教程使用占用更少内存的 Q8 配置。您可在此处获取该模型:https://huggingface.co/litert-community/gemma-3-270m-it

val TASK_GEMMA_3_270M = Task(
  type = TaskType.LLM_PROMPT_LAB,
  icon = Icons.Outlined.Widgets,
  models = mutableListOf(
    Model(
      name = "gemma3-270m-it-q8",
      downloadFileName = "gemma3-270m-it-q8.task",
      url = "https://huggingface.co/litert-community/gemma-3-270m-it/resolve/main/gemma3-270m-it-q8.task",
      sizeInBytes = 318_767_104L
    )
  ),
  description = "Gemma 3 270M (Q8): On-device instruction-following LLM",
  docUrl = "https://ai.google.dev/gemma",
  sourceCodeUrl = "https://github.com/google-ai-edge/gallery",
  textInputPlaceHolderRes = R.string.chat_textinput_placeholder
)

然后,通过将其包含到主任务列表中来注册这个新任务:

/** All tasks */
val TASKS: List<Task> = listOf(
  TASK_LLM_ASK_IMAGE,
  TASK_LLM_CHAT,
  TASK_LLM_PROMPT_LAB,
  TASK_IMAGE_CLASSIFICATION,
  TASK_TEXT_CLASSIFICATION,
  TASK_IMAGE_GENERATION,
  TASK_GEMMA_3_270M
)

以下是新建 LiteRT 任务定义在代码中的作用分解:

  • 定义新任务:变量 TASK_GEMMA_3_270M 在应用的 Tasks.kt 中注册 Gemma 3 270M,使其作为可选项显示在任务列表中。
  • 链接 .task 文件:上述代码指向托管在 Hugging Face 上的 gemma3-270m-it-q8.task 包,应用会在运行时下载并使用。
  • 模型细节:.task 文件包含量化后的模型权重、分词器、元数据与配置,LiteRT 依此高效运行模型。
  • Q8 变体:该量化在内存节省与输出质量间取得平衡,足够轻量以适配手机,同时能良好处理指令跟随。
  • 任务元数据:添加了描述、文档 URL 与源码链接,便于在 Gallery 应用中理解与维护该任务。
  • 任务列表:最终通过将 TASK_GEMMA_3_270M 列入任务列表,确保模型被识别。

步骤 3:修复模型填充

默认情况下,Gallery 应用会从远程 allowlist JSON 填充其模型列表。如果您自定义的 Gemma 3 270M 不在其中,即使您在代码中添加了 TASK_GEMMA_3_270M,它也不会显示在 UI 中。本步骤会在 allowlist 加载后将 270M 模型“嫁接”到LLM Prompt Lab 任务中,确保每次都可选择。

打开 modelmanager/ModelManagerViewModel.kt,找到 loadModelAllowlist() 函数,并在(转换 allowlist 模型的循环之后)追加:

Log.d(TAG, "Adding TASK_GEMMA_3_270M models to LLM_PROMPT_LAB")
            for (gemmaModel in TASK_GEMMA_3_270M.models) {
              if (!TASK_LLM_PROMPT_LAB.models.any { it.name == gemmaModel.name }) {
                Log.d(TAG, "Adding model ${gemmaModel.name} to LLM_PROMPT_LAB")
                TASK_LLM_PROMPT_LAB.models.add(gemmaModel)
              }

这样做的原因如下:

  • Allowlist 重置:应用在启动时会从服务器 allowlist 清空并重建 TASK_LLM_PROMPT_LAB.models。除非您重新注入,本地模型会被清除。
  • 幂等且去重:any { it.name == gemmaModel.name } 保护条件可在 loadModelAllowlist() 再次运行时避免重复。
  • 离线友好:无论 allowlist 来自网络还是磁盘回退,该代码都能工作,并在两种情况下追加模型。

下面是完整函数:

fun loadModelAllowlist() {
    _uiState.update {
      uiState.value.copy(
        loadingModelAllowlist = true, loadingModelAllowlistError = ""
      )
    }
    viewModelScope.launch(Dispatchers.IO) {
      try {
        withTimeoutOrNull(30000L) { // 30 second timeout
          // Load model allowlist json.
          Log.d(TAG, "Loading model allowlist from internet...")
          val data = getJsonResponse<ModelAllowlist>(url = MODEL_ALLOWLIST_URL)
          var modelAllowlist: ModelAllowlist? = data?.jsonObj
          if (modelAllowlist == null) {
            Log.d(TAG, "Failed to load model allowlist from internet. Trying to load it from disk")
            modelAllowlist = readModelAllowlistFromDisk()
          } else {
            Log.d(TAG, "Done: loading model allowlist from internet")
            saveModelAllowlistToDisk(modelAllowlistContent = data?.textContent ?: "{}")
          }
          if (modelAllowlist == null) {
            Log.e(TAG, "Failed to load model allowlist from both internet and disk")
            _uiState.update { 
              uiState.value.copy(
                loadingModelAllowlist = false,
                loadingModelAllowlistError = "Failed to load model list"
              ) 
            }
            return@withTimeoutOrNull
          }
          Log.d(TAG, "Allowlist: $modelAllowlist")
          // Convert models in the allowlist.
          TASK_LLM_CHAT.models.clear()
          TASK_LLM_PROMPT_LAB.models.clear()
          TASK_LLM_ASK_IMAGE.models.clear()          
          try {
            for (allowedModel in modelAllowlist.models) {
              if (allowedModel.disabled == true) {
                continue
              }
              val model = allowedModel.toModel()
              if (allowedModel.taskTypes.contains(TASK_LLM_CHAT.type.id)) {
                TASK_LLM_CHAT.models.add(model)
              }
              if (allowedModel.taskTypes.contains(TASK_LLM_PROMPT_LAB.type.id)) {
                TASK_LLM_PROMPT_LAB.models.add(model)
              }
              if (allowedModel.taskTypes.contains(TASK_LLM_ASK_IMAGE.type.id)) {
                TASK_LLM_ASK_IMAGE.models.add(model)
              }
            }
            // Add models from TASK_GEMMA_3_270M to LLM_PROMPT_LAB
            Log.d(TAG, "Adding TASK_GEMMA_3_270M models to LLM_PROMPT_LAB")
            for (gemmaModel in TASK_GEMMA_3_270M.models) {
              if (!TASK_LLM_PROMPT_LAB.models.any { it.name == gemmaModel.name }) {
                Log.d(TAG, "Adding model ${gemmaModel.name} to LLM_PROMPT_LAB")
                TASK_LLM_PROMPT_LAB.models.add(gemmaModel)
              }
            }
          } catch (e: Exception) {
            Log.e(TAG, "Error processing model allowlist", e)
          }
          // Pre-process all tasks.
          try {
            processTasks()
          } catch (e: Exception) {
            Log.e(TAG, "Error processing tasks", e)
          }
          // Update UI state.
          val newUiState = createUiState()
          _uiState.update {
            newUiState.copy(
              loadingModelAllowlist = false,
            )
          }
          // Process pending downloads.
          try {
            processPendingDownloads()
          } catch (e: Exception) {
            Log.e(TAG, "Error processing pending downloads", e)
          }
        } ?: run {
          // Timeout occurred
          Log.e(TAG, "Model allowlist loading timed out")
          _uiState.update { 
            uiState.value.copy(
              loadingModelAllowlist = false,
              loadingModelAllowlistError = "Model list loading timed out"
            ) 
          }
        }
      } catch (e: Exception) {
        Log.e(TAG, "Error in loadModelAllowlist", e)
        _uiState.update { 
          uiState.value.copy(
            loadingModelAllowlist = false,
            loadingModelAllowlistError = "Failed to load model list: ${e.message}"
          ) 
        }
      }
    }
  }

步骤 4:更新导航逻辑

应用默认假定当前选中的任务与被点击的模型匹配。在您将 Gemma 3 270M 注入 Prompt Lab 任务后(步骤 3),该假设可能不再成立。本步骤将导航逻辑改为“感知模型”,确保点击、深链接与状态恢复始终落到正确界面。

打开 navigation/GalleryNavGraph.kt,按如下方式更新模型导航控制逻辑:

修改前:

onModelClicked = { model ->
  navigateToTaskScreen(
    navController = navController, taskType = curPickedTask.type, model = model
  )
}

修改后:

onModelClicked = { model ->
  Log.d("GalleryNavGraph", "Model clicked: ${model.name}, current task: ${curPickedTask.type}")
  val actualTask = when {
    TASK_LLM_CHAT.models.any { it.name == model.name } -> TASK_LLM_CHAT
    TASK_LLM_PROMPT_LAB.models.any { it.name == model.name } -> TASK_LLM_PROMPT_LAB
    TASK_LLM_ASK_IMAGE.models.any { it.name == model.name } -> TASK_LLM_ASK_IMAGE
    TASK_TEXT_CLASSIFICATION.models.any { it.name == model.name } -> TASK_TEXT_CLASSIFICATION
    TASK_IMAGE_CLASSIFICATION.models.any { it.name == model.name } -> TASK_IMAGE_CLASSIFICATION
    TASK_IMAGE_GENERATION.models.any { it.name == model.name } -> TASK_IMAGE_GENERATION
    else -> curPickedTask
  }
  Log.d("GalleryNavGraph", "Navigating to task: ${actualTask.type} for model: ${model.name}")
  navigateToTaskScreen(
    navController = navController, taskType = actualTask.type, model = model
  )
}

如果某个模型出现在(或被注入到)与当前选择不同的任务中,此代码可确保界面与模型能力匹配。

在处理深链接或状态恢复时,应用可能尝试打开已不可用的模型。若未妥善处理,将导致空白界面。为避免此问题,我们添加安全检查:若模型缺失,则记录警告并重定向用户,具体如下:

// Added error handling for model not found:
if (model != null) {
  Log.d("GalleryNavGraph", "Model found for LlmChat: ${model.name}")
  modelManagerViewModel.selectModel(model)
  LlmChatScreen(/* ... */)
} else {
  Log.w("GalleryNavGraph", "Model not found for LlmChat, modelName: ${it.arguments?.getString("modelName")}")
  // Handle case where model is not found
  androidx.compose.runtime.LaunchedEffect(Unit) {
    navController.navigateUp()
  }
}

此代码确保应用保持可用。它不会卡死或显示空白页,而是通过 navController 安全返回。即使模型缺失,用户流程也能顺畅延续,不破坏整体体验。

步骤 5:安全与可靠性

在移动设备上运行大语言模型与在桌面端不同:存储可能不稳定、内存预算更紧、初始化失败需要妥善处理以避免崩溃。为使 Gemma 3 270M(Q8,约 304MB)在 Android 上更加稳定,我引入了额外的安全检查与故障切换策略。

步骤 5.1:带容差的文件校验

在加载模型前,应用会对 .task 文件进行合理性检查,确保其存在、可读,且大小接近预期(±10%)。这种容差考虑了压缩或打包带来的细微差异,同时能捕捉损坏或不完整的下载。

// File validation and logging
Log.d(TAG, "Checking model file: $modelPath")
Log.d(TAG, "Model file exists: ${modelFile.exists()}")
Log.d(TAG, "Model file can read: ${modelFile.canRead()}")
Log.d(TAG, "Model file size: ${modelFile.length()} bytes")
Log.d(TAG, "Model file absolute path: ${modelFile.absolutePath}")
// Size validation with tolerance (10%)
val expectedSize = model.sizeInBytes
val fileSize = modelFile.length()
val sizeDifference = kotlin.math.abs(fileSize - expectedSize)
val sizeTolerance = (expectedSize * 0.1).toLong()
if (!modelFile.exists() || !modelFile.canRead() || sizeDifference > sizeTolerance) {
  onDone("Model file invalid (missing/unreadable/wrong size).")
  return
}

该片段会在加载前校验模型文件,记录路径、可读性与大小等信息,并检查是否存在、可读以及大小是否匹配预期。若任一检查失败,则通过 onDone() 提前结束,而不是继续推理,避免崩溃风险。

步骤 5.2:感知内存的后端选择

大型模型若激进启动(长输出、GPU 分配峰值)可能导致 OOM。为缓解,我加入逻辑:依据文件大小与设备配置动态选择更安全的后端(CPU vs GPU/NPU),并在二进制过大时收紧最大 tokens。

val preferredBackend = when {
  fileSize > 200_000_000L -> {
    Log.d(TAG, "Model is large (${fileSize / 1024 / 1024}MB), using CPU backend for safety")
    LlmInference.Backend.CPU
  }
  accelerator == Accelerator.CPU.label -> LlmInference.Backend.CPU
  accelerator == Accelerator.GPU.label -> LlmInference.Backend.GPU
  else -> LlmInference.Backend.CPU
}
val baseMaxTokens = 1024 // your default; tune to taste
val maxTokens = if (fileSize > 200_000_000L) {
  kotlin.math.min(baseMaxTokens, 512)
} else {
  baseMaxTokens
}

这样可确保较大的模型默认使用 CPU(更安全但更慢)。代码检查 .task 的字节大小(fileSize),并与阈值比较(maxTokens)。若超过 200MB,则切换到 CPU 推理并将 token 预算减半,从而避免同时 GPU 分配引发的内存峰值和超大生成请求。

步骤 5.3:异常处理

在初始化 LiteRT 时,要区分运行时创建与会话创建。若会话创建失败而仍保持运行时打开,可能导致资源泄漏与内存锁定,进而引发级联失败。为此,我用 try/catch 包裹并显式清理。

val llmInference = try {
  LlmInference.createFromOptions(context, options)
} catch (e: Exception) {
  Log.e(TAG, "Failed to create LlmInference instance", e)
  onDone("Failed to create model instance: ${e.message}")
  return
}
val session = try {
  LlmInferenceSession.createFromOptions(/* ... */)
} catch (e: Exception) {
  Log.e(TAG, "Failed to create LlmInferenceSession", e)
  try {
    llmInference.close()
  } catch (closeException: Exception) {
    Log.e(TAG, "Failed to close LlmInference after session creation failure", closeException)
  }
  onDone("Failed to create model session: ${e.message}")
  return
}

此 try/catch 保证了干净回滚:若运行时创建失败,则不再继续;若会话创建失败,则显式关闭运行时。确保通过 .close() 释放 llmInference 在堆上分配的对象与文件句柄,否则可能遗留 JNI 引用,导致下一次加载崩溃。

步骤 5.4:推理期防护

即使模型成功加载,推理也可能因竞态、空引用或未初始化实例而失败。为增强健壮性,我在完整推理块周围加入空值检查与异常捕获。

try {
  Log.d(TAG, "Starting inference for model '${model.name}' with input: '${input.take(50)}...'")
  val instance = model.instance as LlmModelInstance? ?: run {
    Log.e(TAG, "Model instance is null for '${model.name}'")
    resultListener("Error: Model not initialized", true)
    return
  }
} catch (e: Exception) {
  Log.e(TAG, "Error during inference for model '${model.name}'", e)
  resultListener("Error during inference: ${e.message}", true)
}

上述代码确保仅在模型实例有效时才进行推理,并能干净捕获运行时错误。通过 as LlmModelInstance? 的类型转换检查实例类型是否正确,若为 null 则使用“Elvis 式”分支短路。外层 try/catch 则将异常捕获并反馈为可见的错误信息。

步骤 6:在您的设备上安装并运行应用

完成所有必要修改后,我们就可以运行应用了。要在本地 Android 设备上运行应用,请按以下步骤使用无线调试将设备与 Android Studio 配对。

步骤 6.1:设置 Android 设备

在 Android 设备上打开设置,启用开发者选项,然后打开无线调试。您会看到包含两种配对方式的界面。

Wireless debugging settings on Android device for running gemma 3 270M

步骤 6.2:在 Android Studio 管理设备

在 Android Studio 中打开设备管理器(Device Manager)(图标类似带 Android 标志的手机),点击通过二维码配对。

Pair new device with WiFi

使用您的设备扫描二维码并完成配对。一旦连接,您将在设备管理器下看到设备名称。请确保电脑与 Android 设备连接在同一 WiFi 网络。

Device Manager

步骤 6.3:运行应用

点击设备镜像(Device Mirroring)图标(显示桌面与手机屏幕)。当您看到已镜像的设备屏幕后,点击绿色的运行(Run)图标,将应用部署到您的设备。

步骤 6.4:在设备端下载模型

应用将启动,并打开浏览器窗口,提示您使用 write 令牌登录 Hugging Face,以安全下载模型文件。

登录 HuggingFace 后,点击所选模型的开始下载,模型即会开始下载。本示例默认使用 gemma3-270m-it-q8。现在您可以开始试验该模型。

步骤 7:开始实验

在本教程中,我进行了四个实验:

实验 1:自由写作

我让模型写一段自由体故事,结果如下:

模型能生成结构清晰、围绕主题的文本,但倾向于求稳,略显通用。长输出时常会复用部分句子。因此更适合短文、描述与大纲。不过,其首 token 生成约 0.53 秒,解码速度对设备端推理而言也很出色。

实验 2:改写语气

接着,我提供一段已写好的文字,请 Gemma 3 270M 将其改写为友好语气。

Exp 2: Rewrite tone

指令跟随能力强,能在正式、随意与友好语气间清晰切换,同时保留原意。短文本几乎瞬时完成,适合润色邮件或在不同风格间转换文本。

实验 3:文本摘要

我也将预写文本总结为要点。您可以尝试摘要、要点与关键信息等多种形式。

Exp 3: Summarize text

在本实验中,模型能够总结并抓住要点。面对短到中等长度文档,速度表现极佳。适用场景包括管理摘要、要点高亮或 TL;DR 等。

实验 4:代码片段生成

最后,我让模型编写一个小型代码片段。如下所示:

Exp 4: Code snippet writer

模型擅长生成模板与小型工具函数,但若缺少指引,可能遗漏导入、边界情况或复杂逻辑。短片段生成很快,长补全随上下文增长会稍慢。最适合用于辅助函数与配置。

我已创建这个 GitHub 仓库,供您查看完整项目代码。

结语

本指南按步骤演示了如何运行 Gemma 3 270M:从下载并接入自定义 Android 应用,到处理导航、可靠性与推理期安全。

Gemma 3 具备低计算开销的快速推理、极低功耗、对 INT4 量化的良好支持且性能影响很小,并可完全在设备端运行以保障隐私。

Aashi Dutt

作者

Aashi Dutt

我是一名 Google Developers 机器学习(生成式 AI)领域的专家、Kaggle 三项专家,以及 Women Techmakers 大使,拥有 3 年以上的技术从业经验。2020 年我共同创办了一家健康科技初创公司,目前在佐治亚理工学院攻读计算机科学硕士,专攻机器学习。

主题